MobX Cheat Sheet 🚀
MobX has a pretty small API surface and tries to get out of your way as much as possible. Although minimal, there are few concepts you should be familiar with. This guide is meant to be a crash course in MobX.
Required Packages
Make sure to have the following packages installed for working effectively with MobX
and Flutter
:
The main MobX package that includes Observables
, Actions
, and Reactions
Provides the Observer
widget that auto-renders when the tracked observables change.
A powerful code-generator that greatly improves the developer experience with MobX. It provides annotations like
@observable
, @computed
, @action
which hides all the boilerplate in a separately generated file, *.g.dart
.
These packages should appear in your pubspec.yaml
like below.
dependencies:
mobx: ^2.0.7+5
flutter_mobx: ^2.0.6+1
dev_dependencies:
build_runner: ^2.2.1
mobx_codegen: ^2.0.7
Declaring a Store class
Every store in MobX should be declared with the following boilerplate. This is probably the only boilerplate that
gets repeated. You could make this into a code-snippet in your IDE. Below, we are declaring a Todo
store.
// todo.dart
import 'package:mobx/mobx.dart';
part 'todo.g.dart';
class Todo = _Todo with _$Todo;
abstract class _Todo with Store {
/* rest of the class*/
}
Note that the basename of the part-file must match the containing-file exactly! In the above case, the part file is called
todo.g.dart
, which matches thetodo.dart
file in which it is contained. To generate the part-file, you have to run the following command:flutter pub run build_runner watch --delete-conflicting-outputs
Ya, it looks like a mouthful 🙃 but it does the job!
Adding @observable, @computed, @action
Observables are the reactive state of your store and Actions are semantic operations that mutate them. Computed Observables are read-only properties that depend on other observables and auto-update when any of the dependent observables change.
// contact.dart
import 'package:mobx/mobx.dart';
part 'contact.g.dart';
class Contact = _Contact with _$Contact;
abstract class _Contact with Store {
@observable
String firstName;
@observable
String lastName;
@computed
String get fullName => '$firstName, $lastName';
ObservableList<String> phoneNumbers = ObservableList.of([]);
@action
void addPhone(String phone) {
phoneNumbers.add(phone);
}
// async action
@action
Future<void> fetchPhoneNumbers() async {
final numbers = await contactService.fetchFor(firstName, lastName);
phoneNumbers = ObservableList.of(numbers);
}
}
Reactive wrappers
MobX also comes with a set of wrapper-classes that add the reactive behavior. These include:
ObservableList<T>
ObservableSet<T>
ObservableMap<K,V>
ObservableFuture<T>
ObservableStream<T>
Reactive Extensions
You can convert plain
List
,Map
,Set
,Future
andStream
instances into an observable version with theasObservable()
extension method. For example, in the code-snippet above, you could do:ObservableList<String> phoneNumbers = [].asObservable();
Don't underestimate the @computed
Although @computed
looks like a simple, readonly observable, it can easily be your most powerful tool.
By creating @computed
properties that depend on other observables, you can dramatically simplify the UI code and
eliminate most of the business logic inside your Widgets. It is most often used for hiding conditional logic and
calculating some derived information.
For example, rather than checking if some data is loaded successfully inside a Widget...
Widget build(BuildContext context) {
final store = Provider.of<Contact>(context);
return Observer(
builder: (_) {
if (store.loadOperation != null &&
store.loadOperation.status == FutureStatus.fulfilled) {
return ContactView(store);
}
return Container();
}
);
}
...you can create a @computed
property called hasResults
...
class _Contact with Store {
/* rest of the class */
@observable ObservableFuture<void> loadOperation = null;
@computed
bool get hasResults =>
loadOperation != null &&
loadOperation.status == FutureStatus.fulfilled;
}
...and simplify your widget logic...
Widget build(BuildContext context) {
final store = Provider.of<Contact>(context);
return Observer(
builder: (_) {
if (store.hasResults) {
return ContactView(store);
}
return Container();
}
);
}
...Since a @computed
property is an observable, the Observer
will automatically render when it changes!
Adding reactions
Reactions, as the name suggests, react to changes in observables. Without reactions, it would be a boring system that only produces changes in observables but nothing visible or useful ever happens because there are no reactions!
There are 3 types of reactions: autorun
, reaction
, when
. Each reaction returns a ReactionDisposer
, which when called will dispose
the reaction. Disposing a reaction stops tracking the observables.
autorun
: Is a long-running reaction and starts tracking immediately.
// example.dart
import 'package:mobx/mobx.dart';
part 'example.g.dart';
class Counter = _Counter with _$Counter;
abstract class _Counter with Store {
@observable int counter = 0;
ReactionDisposer _dispose;
void setupReactions() {
_dispose = autorun((_){
print("Count is $counter");
});
}
}
reaction
: Is a long-running reaction and starts tracking only after the first change. It runs the effect when any tracked observables change.
// example.dart
import 'package:mobx/mobx.dart';
part 'example.g.dart';
class Counter = _Counter with _$Counter;
abstract class _Counter with Store {
@observable int counter = 0;
ReactionDisposer _dispose;
void setupReactions() {
_dispose = reaction((_) => counter, (int newValue){
print("Count is now $newValue");
});
}
}
when
: a reaction that waits for a condition to become true before running the effect. After running the effect, it automatically disposes. Thuswhen
is a one-time only reaction!.
// example.dart
import 'package:mobx/mobx.dart';
part 'example.g.dart';
class Counter = _Counter with _$Counter;
abstract class _Counter with Store {
@observable int counter = 0;
ReactionDisposer _dispose;
void setupReactions() {
_dispose = when((_) => counter > 10, (){
print("Count has reached the limit of 10");
});
}
}