Mobx.dart: Migrate MobX for NNBD / null safety

Created on 24 Nov 2020  Β·  51Comments  Β·  Source: mobxjs/mobx.dart

MobX will need to be migrated to NNBD. Most packages have started releasing pre-release -nullsafety versions now that it's in beta support.

https://medium.com/dartlang/announcing-dart-null-safety-beta-87610fee6730

Most helpful comment

The nullsafety pre-releases have been published on pub.dev:

All 51 comments

Yup working on it. Client work is keeping me busy. I started a branch null-safety for it

You can follow in this PR: #603

The nullsafety pre-releases have been published on pub.dev:

Is there a roadmap for mobx codegen?

I used a quick hack to add a nullabilitySuffix for computed and generated types.
https://github.com/markuspaschi/mobx.dart
(not elegant, but working in my project at least πŸ˜„)

I seemed to be blocked without a compatible version of mobx codegen.

The Getting Started πŸš€ section of the official docs tells you to install:
dependencies:
mobx: ^2.0.0-nullsafety.2
flutter_mobx: ^2.0.0-nullsafety.0

Together with:
mobx_codegen: ^1.1.2
^ Which appears to be incompatible with the above packages.

What is the approach to use it for a production application, even if that means not using null safety?

The current combination of packages shown in the docs doesn't work. Or at least didn't work in my case.
To be able to run pub get at all I had to switch to the beta flutter channel.

It would be very useful to publish at least an initial null safety version of mobx_codegen also. Currently, stores are complaining about nullable primitive types. For example, String? is not a valid override of String.

Almost all my dependencies migrated to null safety and the mobx_codegen is the only road blocker. Any update? Thank you.

I have been caught up with client work so not getting the time to migrate mobx_codegen. Although a PR can get this going :-)

I dug into it today and posted some notes here: https://github.com/mobxjs/mobx.dart/pull/603#issuecomment-781047102

basically it is going to be a while because mobx_codegen is blocked on upstream packages who haven't migrated yet. I suggest anyone who wants to use mobx today, remove the code generation and do all of your observables by hand for the time being.

Is there a roadmap for mobx codegen?

I used a quick hack to add a nullabilitySuffix for computed and generated types.
https://github.com/markuspaschi/mobx.dart
(not elegant, but working in my project at least πŸ˜„)

You look to be pretty far along on it! Any chance you can do the same for action and observable types too?

https://pub.dev/packages/build_runner/versions/1.11.4 build_runner has just been migrated as of today.

Today I migrated my app by taking out all of the decorators to use the verbose definitions without code generation. It worked!

Hopefully this can help you migrate the code generator: here's how a computed value looks:

  double get counterMultiplied =>_counterMultiplied.value;
  late final_counterMultiplied = Computed<double>(() {
    return counter * 100;
  });

This is the same as

@computed
double get counterMultiplied => counter * 100;

mobx and flutter_mobx are not enough, mobx_codegen also needs to be migrated.

` Because mobx_codegen 1.1.2 depends on mobx ^1.1.0 and no versions of mobx_codegen match >1.1.2 <2.0.0, mobx_codegen ^1.1.2 requires mobx ^1.1.0. So, because rexi_app depends on both mobx 2.0.0-nullsafety.2 and mobx_codegen ^1.1.2, version solving failed. pub get failed (1; So, because rexi_app depends on both mobx 2.0.0-nullsafety.2 and mobx_codegen ^1.1.2, version solving failed.)

Any new info about mobx_codegen?

Any updates?

Any updates?

Hi null safety has become stable, so many people will need this feature from today!

In addition, we cannot use it without mobx_codegen, otherwise the code will be very verbose...

Hi,

I need it too...please update!

Thanks.

I will probably pick this up next week. In middle of some client work that is keeping me pretty busy.

@pavanpodila This is quite a wonderful library, and I am willing to help. Is there anything I can do to make it work today or tomorrow?

@fzyzcjy , few things that need to be done:

  1. Are all dependent packages migrated to NNBD
  2. Update type checks in mobx_codegen to include the nullability field
  3. Write test code for types with and without nullability and if proper signatures are being produced in output

I think the places to update are few but needs good testing.

@pavanpodila Get it. I will check it when having time :)

@pavanpodila Oops still have non-NNBD dependencies:

(base) ➜  mobx_codegen git:(master) dart pub outdated --mode=null-safety
Showing dependencies that are currently not opted in to null-safety.
[βœ—] indicates versions without null safety support.
[βœ“] indicates versions opting in to null safety.

Package Name     Current  Upgradable  Resolvable           Latest               

direct dependencies:
analyzer         -        βœ—0.41.2     βœ“1.1.0               βœ“1.1.0               
build            -        βœ—1.6.2      βœ—1.6.3               βœ—1.6.3               
build_resolvers  -        βœ—1.5.3      βœ—1.5.4               βœ—1.5.4               
meta             -        βœ“1.3.0      βœ“1.3.0               βœ“1.3.0               
mobx             -        βœ—1.2.1+4    βœ“2.0.0-nullsafety.2  βœ“2.0.0-nullsafety.2  
path             -        βœ“1.8.0      βœ“1.8.0               βœ“1.8.0               
source_gen       -        βœ—0.9.10+3   βœ—0.9.10+3            βœ—0.9.10+3            

dev_dependencies:
build_runner     -        βœ—1.11.1     βœ—1.11.5              βœ—1.11.5              
build_test       -        βœ—0.10.12+1  βœ—1.3.7               βœ—1.3.7               
coverage         -        βœ—0.13.11    βœ“1.0.1               βœ“1.0.1               
logging          -        βœ—0.11.4     βœ“1.0.0               βœ“1.0.0               
mockito          -        βœ—4.1.4      βœ“5.0.0               βœ“5.0.0               
test             -        βœ“1.16.5     βœ“1.16.5              βœ“1.16.5              

No pubspec.lock found. There are no Current versions.
Run `pub get` to create a pubspec.lock with versions matching your pubspec.yaml.

9  dependencies are constrained to versions that are older than a resolvable version.
To update these dependencies, edit pubspec.yaml, or run `dart pub upgrade --null-safety`.

One thing to consider is the code generator should be able to generate both null safe and legacy code, because the package should be backwards compatible so people can upgrade the package prior to migrating their app. If you try to include null safe code prior to migrating then you’ll have analyzer errors.

Perhaps an extra flag indicating to generate null safe code could work.

I found a workaround to at least make flutter pub get run:

dev_dependencies:
  mobx_codegen:
    git:
      url: https://github.com/fzyzcjy/mobx.dart.git
      ref: 026dcd073001fd928f43ad74fb8a89a9a0e6d4ad
      path: mobx_codegen

and in that commit, I loosen the constraints (see https://github.com/fzyzcjy/mobx.dart/blob/master/mobx_codegen/pubspec.yaml)

mobx: ">=1.1.0 <3.0.0"

It won’t generate null safe code though and you won’t be able to run the app.

There will be analyzer errors because the computed values and other things won’t be typed correctly with null safety.

My workaround was to remove all of my decorator usage and do things verbosely. After that I was able to run my app after migrating and it works great!

Here's a short guide I wrote to convert to non-decorated syntax if you want to migrate now:


Migration example

Before:

@observer
var accountLocked = false;

@action
void setAccountLocked(bool val) => accountLocked = val;

After:

// Note: `Action` conflicts with something in the material library so it's best to import mobx with a name.
import 'package:mobx/mobx.dart' as mbx;

// Replaces observer part
final _accountLocked = mbx.Observable(false);
bool get accountLocked => _accountLocked.value;
set accountLocked(bool newValue) =>
    mbx.Action(() => _accountLocked.value = newValue)();

// Replaces action setter
void setAccountLocked(bool val) => accountLocked = val;

Simple example of a store

class VanillaStore {
  var _counter = Observable(0);
  int get counter => _counter.value;
  set counter(int newValue) => Action(() => _counter.value = newValue)();

  void setCounter(int newVal) {
    counter = newVal;
  }
}

To make things easier in vscode, there is a new code snippet you can create by typing obs and hitting enter. From here there are 3 variables to enter (pressing tab after each one):

  1. The variable name
  2. The default value
  3. The variable type

That will completely generate the observable snippet.

Add this to your .vscode/dar.code-snippets file:

"MobX observable": {
    "prefix": "obs",
    "body": [
      "final _${1:name} = mbx.Observable(${2:defaul_val});",
      // "@JsonKey(defaultValue: ${2:defaul_val})",
      "${3:type} get ${1:name} => _${1:name}.value;",
      "set ${1:name}(${3:type} newValue) =>",
      "   mbx.Action(() => _${1:name}.value = newValue)();",
    ]
  },
  "MobX computed value": {
    "prefix": "cptd",
    "body": [
      "${2:type} get ${1:name} => _${1:name}.value;",
      "late final _${1:name} = mbx.Computed<${2:type}>(() {",
      "  //",
      "});"
    ]
  },
  "MobX observable list": {
    "prefix": "obsl",
    "body": [
      "final _${1:name} =",
      "mbx.Observable(mbx.ObservableList<${2:type}>.of([]));",
      "@JsonKey(defaultValue: <${2:type}>[])",
      "List<${2:type}> get ${1:name} =>",
      "_${1:name}.value;",
      "set ${1:name}(List<${2:type}> newValue) =>",
      "mbx.Action(() =>",
      "_${1:name}.value = mbx.ObservableList.of(newValue))();"
    ]
  }

ObservableMap example

Observable map is a bit more boilerplate but you basically want to create an Observable which wraps an ObservableMap. ObservableMap notifies when keys/values change, and the Observable will notify when the entire ObservableMap instance is changed.

  final _ratesMap =
      Observable(ObservableMap<String, Map<String, double>>.of({}));
  @JsonKey(defaultValue: <String, Map<String, double>>{})
  Map<String, Map<String, double>> get ratesMap => _ratesMap.value;
  set ratesMap(Map<String, Map<String, double>> newValue) =>
      Action(() => _ratesMap.value = ObservableMap.of(newValue))();

ObservableList example

Observable list is the same concept as ObservableMap. You can also use the snippet obsl.

  final _currencies = mbx.Observable(mbx.ObservableList<Currency>.of([]));
  @JsonKey(defaultValue: <Currency>[])
  List<Currency> get currencies => _currencies.value;
  set currencies(List<Currency> newValue) =>
      mbx.Action(() => _currencies.value = mbx.ObservableList.of(newValue))();

Computed values

You can also use the vscode snippet cptd to generate the code for you.

int get counterTimesOneHundred => _counterTimesOneHundred.value;
late final _counterTimesOneHundred = Computed<int>(() {
  return counter * 100;
});

json_serializable

For json serialization, it's assumed that types are nullable by default. This means the generated code can initialize the class with the members set to null.

To avoid this, any time you have a non nullable member, provide it a default value. The only catch is this default value must be a const.

If you need to use something that isn't a const, you can catch it in your setter to ensure you never set a null value.

The JsonKey should go on the observable's getter.

Full example of vanilla store with json serialization:

import 'package:json_annotation/json_annotation.dart';
import 'package:mobx/mobx.dart';

part 'vanilla_store.g.dart';

@JsonSerializable()
class VanillaStore {
  VanillaStore();

  /// json_serializable generated functions
  factory VanillaStore.fromJson(Map<String, dynamic> json) =>
      _$VanillaStoreFromJson(json);
  Map<String, dynamic> toJson() => _$VanillaStoreToJson(this);

  var _counter = Observable(0);
  @JsonKey(defaultValue: 0)
  int get counter => _counter.value;
  set counter(int newValue) => Action(() => _counter.value = newValue)();

  void setCounter(int newVal) {
    counter = newVal;
  }
}

This results in the generated code:

VanillaStore _$VanillaStoreFromJson(Map<String, dynamic> json) {
  return VanillaStore()..counter = json['counter'] as int? ?? 0;
}

Map<String, dynamic> _$VanillaStoreToJson(VanillaStore instance) =>
    <String, dynamic>{
      'counter': instance.counter,
    };

Notice how it provides a default value for counter in case the map value is missing (null).

From the dart docs:

Even when all the int values in the map are non-null, when you use an invalid key to do a map lookup, the returned value is null.

More here: https://dart.dev/null-safety#map-types

Ignoring observables in json serialization

Sometimes you don't want to persist a value. In that case, you have to add the JsonKey(ignore: true) decorator. For our observables, the place to add that is on its getter again:

  var _foo = Observable(0);
  @JsonKey(ignore: true)
  int get foo => _foo.value;
  set foo(int newValue) => Action(() => _foo.value = newValue)();

I have started working on https://github.com/fzyzcjy/mobx.dart and will push my commits & make a PR soon.

@fzyzcjy , few things that need to be done:

  1. Are all dependent packages migrated to NNBD
  2. Update type checks in mobx_codegen to include the nullability field
  3. Write test code for types with and without nullability and if proper signatures are being produced in output

I think the places to update are few but needs good testing.

@pavanpodila I will firstly work on "2.". (since "1" reveals some dependent packages have not done yet) The mobx_codegen itself will not be NNBD, but the code that is generates will be after the update.

I have created a PR here (any suggestions and updates are welcomed!): https://github.com/mobxjs/mobx.dart/pull/631

Hi null safety has become stable, so many people will need this feature from today!

In addition, we cannot use it without mobx_codegen, otherwise the code will be very verbose...

The fact that 4-5 months ago Dart/Flutter team said quite explicitly and literally to don't use null-safety in production, and that now they release it as "stable" forcing most apps to migrate to nullsafetly all of a sudden, AND that they literally kept it secret the past two months that they would do this (to hype this weekend's crappy event) is bullshit. It's exciting from a tinkerer's perspective, but it's a very shitty practice for developers who rely on things not breaking.

Hi null safety has become stable, so many people will need this feature from today!
In addition, we cannot use it without mobx_codegen, otherwise the code will be very verbose...

The fact that 4-5 months ago Dart/Flutter team said quite explicitly and literally to don't use null-safety in production, and that now they release it as "stable" forcing most apps to migrate to nullsafetly all of a sudden, AND that they literally kept it secret the past two months that they would do this (to hype this weekend's crappy event) is bullshit. It's exciting from a tinkerer's perspective, but it's a very shitty practice for developers who rely on things not breaking.

It’s not β€œforced” or β€œrequired” and it’s been in beta for months. It’s also fully backwards compatible. Your comment here hasn’t contributed anything meaningful to the discussion.

I am actually developing on the master branch and put off migrating my app to null-safety per their recomandation, and yet after 3rd of march - where the master branch (as it was months ago) became the stable branch - I have all kinds of stuff broken. On the stable branch. How is it backwards compatible? And "being in beta for months" is not an excuse, they should have been a warning or announcement that the beta is ending.

It's not a smartphone that needs hype and secrets and big reveals. There should not be big reveals.

As for adding useful things to the discussion, this "big reveal" is hurting and will hurt mobx and similar packages - as evidenced by the demands in this thread. That's what I was commenting about.

Just downgrade to an older version of flutter.

Anyways please keep this discussion related to mobx nnbd migration. Thanks.

Just my two cents on the issue, as far as I can see a lot of people are already working on getting a null-safe version of codegen out, but till then, I've found a workaround of sorts I wanted to share in case anyone needs MobX but its not really feasible to manually write all the boilerplate.

The workaround is basically mixing legacy code with null safe code, resulting in _unsound null safety_ that however does run, and except for lacking some static analysis help, doesn't really cause much problems. It's mentioned here , and what you do is run the app with the flag --no-sound-null-safety , so flutter run --no-sound-null-safety or dart --no-sound-null-safety run is the full command.

Hope this helps! Not the ideal solution, but till the awesome people working on this can push out a stable release, this should serve as a bandaid.

Of course, you use this command with the not null safe versions of mobx , flutter_mobx and mobx_codegen respectively.

@tanzilzubair I have made a PR (merged already) for generating null safety code here: https://github.com/mobxjs/mobx.dart/pull/634

You may reference the null-safety branch of this github repo to use all nullsafety features :) (The pub seems to have not been upgraded yet)

Folks, null-safe versions of mobx, flutter_mobx and mobx_codegen have been published on pub.dev. Please try it out and report any issues πŸš€

To me it is working :)
Thanks.

I am not sure there is much sense in it, but still.. late observables do not work (mobx_codegen: 2.0.0-nullsafety.2):

Example:

@observable
late String name;

@action
Future<void> init() async {
  name = await _readNameFromSomewhereAsync();
}

Generated code will have setter that uses getter before setting value, which apparently throws 'Field 'name' has not been initialized.':

  @override
  set name(String value) {
    _$nameAtom.reportWrite(value, super.name /* fails */, () {
      super.name = value;
    });
  }

@radzish I also observe that. My solution was to not use late as a workaround... (not elegant, I agree)

Any news regarding the late issue ?

Why not assign an initial value or make it nullable ?

Why not assign an initial value or make it nullable ?

In my case I want to save current user information there, and if I will make it nullable then I must add ! everywhere in the project, or I can just make it late

here is my workaround just until mobX will support late Observables

  @observable
  CurrentUser? _currentUser;

  @computed
  CurrentUser get currentUser => _currentUser!;

@NarHakobyan IMHO the "@computed" is not needed if used this way, and adding it will introduce (maybe slight) overhead. (I am not an expert and probably @pavanpodila knows better than me)

The late keyword doesn't mean the same thing as a nullable type. It means that the variable will get initialized at some point, that a null value doesn't make any sense semantically, hence causes error if we try to access it when uninitialized, so there is no need for a null check. If we would want to implement it in mobx codegen, we would have to store a boolean saying wether or not it has been initialized and throw if accessed before initialization. This is needed in my opinion, if we do not want to use value! everywhere in our code.

I can add that it is useless to report write on initialization since we can be sure that it has not been read before (it would have thrown).

The late keyword doesn't mean the same thing as a nullable type.

No, it's a lot worse than that. The late keyword suppresses complier errors and warnings, but keeps the variable nullable.
You "promise" the compiler that you will initialise the variable before using it. Just like the hordes of past programmers who promised they would always check that their variables are not null...

late keyword should be used as a last resort, and the fact that I had lateinitialization error thrown by the mobx package when my observable is non-nullable, is suboptimal. mobx basically turns my non-nullable types into nullable types.

Maybe we need to start using extension methods for mobx and get rid of mobx_codegen

No, it's a lot worse than that. The late keyword suppresses complier errors and warnings, but keeps the variable nullable.

I do not agree with that. A late variable cannot be assigned null nor checked for null value since it doesn't mean anything. For example, if a property depends on another one during initialization, then the late keyword has to be used. Of course in that case the compiler cannot do anything to help you here, but do you really prefer to put ! everywhere in your code when it's clear it cannot be null ?

I don't think this is the right place to debate about this. The late keyword IS a Dart feature so it needs to be supported by mobx.

Just wanting to revive this topic. I just finished migrating an app to null safety and I am very disappointed to find that I'm forced to make variables nullable that _should not be nullable_.

Any update on this? MobX should support late variables

This was done a few months back. Closing.

Was this page helpful?
0 / 5 - 0 ratings