Mobile Development 19 min read

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.

ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
Understanding MobX Data Binding and Update Process in Flutter

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.

DartFluttermobile developmentState ManagementMobX
ByteDance Dali Intelligent Technology Team
Written by

ByteDance Dali Intelligent Technology Team

Technical practice sharing from the ByteDance Dali Intelligent Technology Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.