Understanding MobX Data Binding and Update Process in Flutter
This article explains how MobX implements data binding and update mechanisms in Flutter, compares it with other state‑management frameworks, walks through the internal Observer‑Atom‑Reaction workflow with code examples, and presents common pitfalls and best‑practice recommendations for mobile developers.
Author: Dali Intelligent Client Team – Wang Jiang
The Dali tutoring project uses Flutter extensively across many tabs such as course details, community, language, and personal info, making state management a critical topic.
Unlike native development, which uses imperative UI updates (e.g., setText, setImageUrl), Flutter builds UI declaratively, influencing how update logic is expressed.
Flutter’s basic state‑update API is setState , but complex scenarios require cross‑level or sibling component sharing, so a dedicated state‑management framework is needed.
Framework Selection
We evaluated open‑source options BLoC, Redux, and MobX.
BLoC leverages Dart streams, offering a ReactiveX‑style approach, but its single‑stream model can be too abstract for some pages.
Redux provides predictable, traceable state with pure‑function reducers, at the cost of boilerplate and a functional‑programming learning curve.
MobX offers transparent reactivity, high usability, OOP‑friendly syntax, and rapid iteration, though its freedom can lead to inconsistent code styles without team conventions.
Given our project’s size and fast iteration cadence, we chose MobX for its simplicity and speed, focusing this article on its data‑binding and update flow.
Data Binding Process
Observer widgets and @observable fields establish a binding relationship.
From reportRead() Onward
Atom.reportRead()
When counter.value is accessed, the generated .g.dart getter calls _valueAtom.reportRead() . Each @observable generates an Atom that wraps the field.
final _$valueAtom = Atom(name: '_Counter.value');
@override
int get value {
_$valueAtom.reportRead();
return super.value;
}ReactiveContext._reportObserved()
The global ReactiveContext singleton records the observed Atom in the current Derivation ’s _newObservables set.
void _reportObserved(Atom atom) {
final derivation = _state.trackingDerivation;
if (derivation != null) {
derivation._newObservables.add(atom);
if (!atom._isBeingObserved) {
atom
.._isBeingObserved = true
.._notifyOnBecomeObserved();
}
}
}Back to the Start
Stack Information
The stack shows the flow from main.dart to MyHomePage.build , highlighting where Observer interacts.
Observer‑Related Widget and Element
Observer is a StatelessObserverWidget whose element mixes in ObserverElementMixin . The element creates a ReactionImpl (a Derivation ) that holds _newObservables .
ObserverElementMixin.mount()
@override
void mount(Element parent, dynamic newSlot) {
_reaction = _widget.createReaction(invalidate, onError: (e, _) { ... }) as ReactionImpl;
...
}ObserverElementMixin.build()
Widget build() {
reaction.track(() {
built = super.build();
});
...
}ReactionImpl.track() → ReactiveContext.trackDerivation()
void track(void Function() fn) {
...
_context.trackDerivation(this, fn);
...
}
T trackDerivation
(Derivation d, T Function() fn) {
final prevDerivation = _startTracking(d);
result = fn();
_endTracking(d, prevDerivation);
return result;
}ReactiveContext._startTracking()
Derivation _startTracking(Derivation derivation) {
final prevDerivation = _state.trackingDerivation;
_state.trackingDerivation = derivation;
_resetDerivationState(derivation);
derivation._newObservables = {};
return prevDerivation;
}Observer.builder Invocation
class Observer extends StatelessObserverWidget implements Builder {
@override
Widget build(BuildContext context) => builder(context);
}During the builder execution, any @observable getter triggers reportRead , adding its Atom to the current derivation’s _newObservables .
ReactiveContext._endTracking()
void _endTracking(Derivation currentDerivation, Derivation prevDerivation) {
_state.trackingDerivation = prevDerivation;
_bindDependencies(currentDerivation);
}
void _bindDependencies(Derivation derivation) {
final staleObservables = derivation._observables.difference(derivation._newObservables);
final newObservables = derivation._newObservables.difference(derivation._observables);
for (final observable in newObservables) {
observable._addObserver(derivation);
if (observable is Computed) {
// handle computed dependencies
}
}
for (final ob in staleObservables) {
ob._removeObserver(derivation);
}
derivation
.._observables = derivation._newObservables
.._newObservables = {};
}Thus the Atom gains the Derivation as an observer, completing the binding.
Conclusion of Binding
The Observer widget holds a ReactionImpl which stores a set of observed @observable atoms; those atoms register the reaction as an observer, establishing the reactive link.
Data Update Process
When an @observable is modified, its generated setter calls Atom.reportWrite , which updates the value and queues the associated reaction.
reportWrite()
The setter looks like:
@override
set value(int value) {
_$valueAtom.reportWrite(value, super.value, () { super.value = value; });
}propagateChanged()
void propagateChanged(Atom atom) {
if (atom._lowestObserverState == DerivationState.stale) return;
atom._lowestObserverState = DerivationState.stale;
for (final observer in atom._observers) {
if (observer._dependenciesState == DerivationState.upToDate) {
observer._onBecomeStale();
}
observer._dependenciesState = DerivationState.stale;
}
}ReactionImpl._onBecomeStale()
@override
void _onBecomeStale() {
schedule();
}
void schedule() {
_context..addPendingReaction(this)..runReactions();
}ReactiveContext.addPendingReaction()
void addPendingReaction(Reaction reaction) {
_state.pendingReactions.add(reaction);
}ReactiveContext.runReactions()
void runReactions() {
for (final reaction in remainingReactions) {
reaction._run();
}
_state.pendingReactions = [];
_state.isRunningReactions = false;
}ReactionImpl._run()
@override
void _run() {
_onInvalidate();
}_onInvalidate ultimately calls markNeedsBuild() , marking the widget dirty so Flutter rebuilds it on the next frame.
Conclusion of Update
Changing an @observable triggers reportWrite , which enqueues its reaction; the reaction runs, calls markNeedsBuild , and the UI updates.
Common Pitfalls (Bad Cases)
Bad Case 1
Widget build(BuildContext context) {
return Observer(builder:(context){
Widget child;
if (store.showImage) {
child = Image.network(store.imageURL);
} else {
// ...
}
});
}If store.showImage is false on the first build, store.imageURL never gets observed, so later changes won’t trigger UI updates.
Bad Case 2
Widget build(BuildContext context) {
return Observer(builder:(context){
Widget child;
if (store.a && store.b && store.c) {
child = Image.network(store.imageURL);
} else {
// ...
}
});
}Short‑circuit evaluation means if store.a is false initially, store.b and store.c aren’t observed.
Bad Case 3
class WidgetA extends StatelessWidget{
Widget build(BuildContext context) {
Observer(builder:(context){
return TestWidget();
});
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Text('${counter.value}'),
onTap: () { counter.increment(); },
);
}
}The Observer wraps WidgetA , but the actual read of counter.value occurs inside WidgetB , which isn’t directly observed, so UI doesn’t refresh.
Best Practice (Good Case)
Extract observable reads before the conditional logic so all variables are observed.
Widget build(BuildContext context) {
return Observer(builder:(context){
Widget child;
bool a = store.a;
bool b = store.b;
bool c = store.c;
if (a && b && c) {
child = Image.network(store.imageURL);
} else {
// ...
}
});
}Additional notes: MobX batches reactions via startBatch to reduce redundant updates, and developers can optimise reportWrite by diffing old and new values.
For further exploration, readers are encouraged to inspect the MobX source code.
ByteDance Dali Intelligent Technology Team
Technical practice sharing from the ByteDance Dali Intelligent Technology Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.