At the heart of MobX are three important concepts: Observables, Actionsand Reactions.
Observables
Observables represent the reactive-state of your application. They can be simplescalars to complex object trees. By defining the state of the application as atree of observables, you can expose a reactive-state-tree that the UI (orother observers in the app) consume.
The term
reactive data
means that a change to data causes a notification tobe fired to every interested observer. It is the application of the classicObserver Design Pattern.
A simple reactive-counter is represented by the following observable:
import 'package:mobx/mobx.dart';
final counter = Observable(0);
More complex observables, such as classes, can be created as well.
class Counter {
Counter() {
increment = Action(_increment);
}
final _value = Observable(0);
int get value => _value.value;
set value(int newValue) => _value.value = newValue;
late Action increment;
void _increment() {
_value.value++;
}
}
On first sight, this does look like some boilerplate code which can quickly goout of hand! This is why we added to the mixthat allows you to replace the above code with the following:
import 'package:mobx/mobx.dart';
part 'counter.g.dart';
class Counter = CounterBase with _$Counter;
abstract class CounterBase with Store {
@observable
int value = 0;
@action
void increment() {
value++;
}
}
Note the use of annotations to mark the observable properties of the class. Yes,there is some header boilerplate here but its fixed for any class. As you buildmore complex classes, this boilerplate will fade away and you will mostly focuson the code within the braces.
Note: Annotations are available via the package.
The above code is part of the Counter Example.
For those who have experience with the JavaScript version of MobX, it'simportant to note that deep observability doesn't apply in the Dart version ofMobX. This is because Dart is a statically typed language and there's nosupport for reflection (no
dart:mirrors
) in Flutter applications. Therefore,if a complex object is marked as@observable
then MobX will not be able toautomatically track changes to the object's fields. Should you need changetracking at the field level, it's better to mark them individually with the@observable
annotation.
Computed Observables
What can be derived, should be derived. Automatically.
The state of your application consists of core-state andderived-state. The core-state is state inherent to the domain you aredealing with. For example, if you have a Contact
entity, the firstName
andlastName
form the core-state of Contact
. However, fullName
isderived-state, obtained by combining firstName
and lastName
.
Such derived state, which depends on core-state or other derived-state iscalled a Computed Observable. It is automatically kept in sync when itsunderlying observables change.
State in MobX = Core-State + Derived-State
import 'package:mobx/mobx.dart';
part 'contact.g.dart';
class Contact = ContactBase with _$Contact;
abstract class ContactBase with Store {
@observable
String firstName;
@observable
String lastName;
@computed
String get fullName => '$firstName, $lastName';
}
In the example above fullName
is automatically kept in sync if eitherfirstName
and/or lastName
changes.
Actions
Actions are how you mutate the observables. Rather than mutating them directly,actions add a semantic meaning to the mutations. For example, instead of justdoing value++
, firing an increment()
action carries more meaning. Besides,actions also batch up all the notifications and ensure the changes are notifiedonly after they complete. Thus, the observers are notified only upon the atomiccompletion of the action.
Note that actions can also be nested, in which case the notifications go outwhen the top-most action has completed.
final counter = Observable(0);
final increment = Action((){
counter.value++;
});
When creating actions inside a class, you can take advantage of annotations!
import 'package:mobx/mobx.dart';
part 'counter.g.dart';
class Counter = CounterBase with _$Counter;
abstract class CounterBase with Store {
@observable
int value = 0;
@action
void increment() {
value++;
}
}
Reactions
Reactions complete the MobX triad of observables, actions andreactions. They are the observers of the reactive-system and get notifiedwhenever an observable they track is changed. Reactions come in few flavors aslisted below. All of them return a ReactionDisposer
, a function that can becalled to dispose the reaction.
One striking feature of reactions is that they automatically track all theobservables without any explicit wiring. The act of reading an observablewithin a reaction is enough to track it!
The code you write with MobX appears to be literally ceremony-free!
ReactionDisposer autorun(Function(Reaction) fn)
Runs the reaction immediately and also on any change in the observables usedinside fn
.
import 'package:mobx/mobx.dart';
final greeting = Observable('Hello World');
final dispose = autorun((_){
print(greeting.value);
});
greeting.value = 'Hello MobX';
// Done with the autorun()
dispose();
// Prints:
// Hello World
// Hello MobX
ReactionDisposer reaction<T>(T Function(Reaction) fn, void Function(T) effect)
Monitors the observables used inside the fn()
function and runs the effect()
when the tracking function returns a different value. Only the observablesinside fn()
are tracked.
import 'package:mobx/mobx.dart';
final greeting = Observable('Hello World');
final dispose = reaction((_) => greeting.value, (msg) => print(msg));
greeting.value = 'Hello MobX'; // Cause a change
// Done with the reaction()
dispose();
// Prints:
// Hello MobX
ReactionDisposer when(bool Function(Reaction) predicate, void Function() effect)
Monitors the observables used inside predicate()
and runs the effect()
when it returns true
. After the effect()
is run, when
automaticallydisposes itself. So you can think of when as a one-time reaction
. You canalso dispose when()
pre-maturely.
import 'package:mobx/mobx.dart';
final greeting = Observable('Hello World');
final dispose = when((_) => greeting.value == 'Hello MobX', () => print('Someone greeted MobX'));
greeting.value = 'Hello MobX'; // Causes a change, runs effect and disposes
// Prints:
// Someone greeted MobX
Future<void> asyncWhen(bool Function(Reaction) predicate)
Similar to when
but returns a Future
, which is fulfilled when thepredicate()
returns true. This is a convenient way of waiting for thepredicate()
to turn true
.
final completed = Observable(false);
void waitForCompletion() async {
await asyncWhen(() => completed.value == true);
print('Completed');
}
Observer
One of the most visual reactions in the app is the UI. The Observer widget(which is part of the ), provides a granularobserver of the observables used in its
builder
function. Whenever theseobservables change, Observer
rebuilds and renders.
Below is the Counter example in its entirety.
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
part 'counter.g.dart';
class Counter = CounterBase with _$Counter;
abstract class CounterBase with Store {
@observable
int value = 0;
@action
void increment() {
value++;
}
}
class CounterExample extends StatefulWidget {
const CounterExample();
@override
CounterExampleState createState() => CounterExampleState();
}
class CounterExampleState extends State<CounterExample> {
final Counter counter = Counter();
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: const Text('MobX Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Observer(
builder: (_) => Text(
'${counter.value}',
style: const TextStyle(fontSize: 40),
)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
Deeper coverage
There are some articles written byMichel Weststrate which go into thephilosophy of MobX. Definitely worth reading them.