Pick up another article. The way of Flutter state management (3)
This article mainly introduces the shuttle ﹣ mobx
flutter_mobx
Edition:
dependencies:
mobx: ^0.4.0
flutter_mobx: ^0.3.4
dev_dependencies:
build_runner: ^1.3.1
mobx_codegen: ^0.3.11
Document: https://mobx.pub/
concept
object | Explain | |
---|---|---|
Observables | Represents a reactive state, which can be a normal dart object, It can also be a state tree, and change will trigger reaction |
|
Computed | Calculated from multiple Observables sources The value it should output, with cache, will be cleared if it is not used, Source change triggers recalculation and change triggers reaction |
|
Actions | Responding to changes in Observables | |
Reactions | Where the response to the Action, Observable and Computed three elements occurs, Can be Widget / function |
|
Observer | A specific implementation of the above Reaction, which is used in the Flutter to respond to package needs Subtree of Observable |
Concept map (from mobx.pub):
Use example
From the official counter Demo
- Define Store, create counter.dart
// Include generated file part 'counter.g.dart'; ///Generate code by annotation parsing // This is the class used by rest of your codebase class Counter = _Counter with _$Counter; // The store-class abstract class _Counter with Store { @observable int value = 0; @action void increment() { value++; } }
- main.dart
final counter = Counter(); // 1. Initialize Store void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'MobX', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('MobX Counter'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), // Wrapping in the Observer will automatically re-render on changes to counter.value Observer( ///2. Using counter with Observer package will automatically establish subscription relationship builder: (_) => Text( '${counter.value}', style: Theme.of(context).textTheme.display1, ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: counter.increment, ///3. Call the Observer's setter method to notify the update tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
Icon
Key objects
Take the above counter example to analyze the key objects in the source code
_$Counter
This object is generated by the code generator, which mainly implements the ability to extend the Observable of the custom dart object
mixin _$Counter on _Counter, Store { ///Store is just a mixin ///"Counter" is our custom state object final _$valueAtom = Atom(name: '_Counter.value'); ///According to the variable identified by @ observable, the generated Atom object is used to implement observer pattern with Reactions @override int get value { ///getter of attribute value is generated according to @ observable _$valueAtom.context.enforceReadPolicy(_$valueAtom); ///It is used to limit whether this method can be called in non Actions and Reactions. It is allowed by default. Otherwise, an assert exception will be thrown _$valueAtom.reportObserved(); ///Register observer (Reaction) return super.value; //Return value } @override set value(int value) { _$valueAtom.context.conditionallyRunInAction(() { ///conditionallyRunInAction is used to determine whether the security policy written and the trace containing action super.value = value; ///Place of assignment _$valueAtom.reportChanged(); ///Notify the Observer registered in valueAtom to refresh data }, _$valueAtom, name: '${_$valueAtom.name}_set'); } final _$_CounterActionController = ActionController(name: '_Counter'); ///Generated according to @ action, used to record action calls @override void increment() { final _$actionInfo = _$_CounterActionController.startAction(); ///Record start try { return super.increment(); ///The actual execution of the defined increment method } finally { _$_CounterActionController.endAction(_$actionInfo); ///Record complete } } }
The $valueAtom.context in the above code snippet is the global MainContext taken by default in each Atom. See Atom structure:
class Atom { factory Atom( {String name, Function() onObserved, Function() onUnobserved, ReactiveContext context}) => Atom._(context ?? mainContext, ///Note that here, mainContext will be used when parameters are not passed name: name, onObserved: onObserved, onUnobserved: onUnobserved); ... }
Here are some key methods:
- _$valueAtom.context.conditionallyRunInAction
void conditionallyRunInAction(void Function() fn, Atom atom, {String name, ActionController actionController}) { if (isWithinBatch) { ///When executing in action or reaction, enter here directly enforceWritePolicy(atom); ///Check the write permission, such as whether it can be written outside of non action fn(); ///Where real assignments are performed } else { ///Execute in non action or transaction final controller = actionController ?? ActionController( context: this, name: name ?? nameFor('conditionallyRunInAction')); final runInfo = controller.startAction(); ///Record action start try { enforceWritePolicy(atom); fn(); } finally { controller.endAction(runInfo); ///Record end of action } } }
- _$valueAtom.reportObserved()
/// Atom void reportObserved() { _context._reportObserved(this); } /// ReactiveContext void _reportObserved(Atom atom) { final derivation = _state.trackingDerivation; ///Take out the currently executing reactions or calculations if (derivation != null) { derivation._newObservables.add(atom); ///Bind the current atom to the derivation if (!atom._isBeingObserved) { ///If atom has not been added to the observation before, execute here atom .._isBeingObserved = true .._notifyOnBecomeObserved(); ///Notify all listener s of Observable - it becomes Observable } } }
As can be seen above, atom is added to the listening collection of the current generation. That is to say, the generation holds atom, but when the atom changes, it needs to be notified to the generation. Continue to see below
- _$valueAtom.reportChanged()
/// Atom void reportChanged() { _context ..startBatch() ///The batch count + 1 records the depth of the current batch, which is used to track the execution depth of action s ..propagateChanged(this) ///Notify the observer (i.e. Derivation) registered in atom of data change ..endBatch(); ///After execution, count - 1 for batch and check whether the execution depth of batch is set to 0. Here is layer optimization } /// ReactiveContext void propagateChanged(Atom atom) { ... atom._lowestObserverState = DerivationState.stale; for (final observer in atom._observers) { if (observer._dependenciesState == DerivationState.upToDate) { observer._onBecomeStale(); ///Notify all registered i.e. the change of the innovation data } observer._dependenciesState = DerivationState.stale; } } void endBatch() { if (--_state.batch == 0) { ///Optimization: when the current level of execution change does not return to 0, skip the final reaction response, and only go to the following logic after all the execution is completed (personal understanding: because it is a single thread, the recursive situation should be considered here, such as calling action again in action) runReactions(); ///Notify pending reactions of data changes /// List<Reaction> pendingReactions = []; /// The reactions that must be triggered at the end of a `transaction` or an `action` for (var i = 0; i < _state.pendingUnobservations.length; i++) { ///Deal with disconnected observations like dispose here final ob = _state.pendingUnobservations[i] .._isPendingUnobservation = false; if (ob._observers.isEmpty) { if (ob._isBeingObserved) { // if this observable had reactive observers, trigger the hooks ob .._isBeingObserved = false .._notifyOnBecomeUnobserved(); } if (ob is Computed) { ob._suspend(); } } } _state.pendingUnobservations = []; } }
Basically, "Counter" refers to the variable extension getter and setter methods of @ observable annotation. In getter, the atom corresponding to the variable is bound into the currently executed derivation, and in setter, the set of "observers" in atom is notified.
@The method of action annotation will be included in the control of $\
But when are the elements in atom. \
Observer
As a responsive component of UI in fluent, take a look at the class diagram
As shown in the figure above, StatelessObserverWidget extends StatelessWidget. The framework mainly extends functions through the observer widget mixin and observer element mixin
- ObserverWidgetMixin
mixin ObserverWidgetMixin on Widget { String getName(); ReactiveContext getContext() => mainContext; Reaction createReaction( Function() onInvalidate, { Function(Object, Reaction) onError, }) => ReactionImpl( getContext(), onInvalidate, name: getName(), onError: onError, ); }
Basically, it extends 1) create reaction 2) get mainContext global responsive context
- ObserverElementMixin
mixin ObserverElementMixin on ComponentElement { ReactionImpl get reaction => _reaction; ReactionImpl _reaction; ///Response class of package ObserverWidgetMixin get _widget => widget as ObserverWidgetMixin; @override void mount(Element parent, dynamic newSlot) { ///Create Reaction when mounting Element _reaction = _widget.createReaction(invalidate, onError: (e, _) { FlutterError.reportError(FlutterErrorDetails( library: 'flutter_mobx', exception: e, stack: e is Error ? e.stackTrace : null, )); }) as ReactionImpl; super.mount(parent, newSlot); } void invalidate() => markNeedsBuild(); ///When the Observable changes, it will notify that the label is dirty @override Widget build() { Widget built; reaction.track(() { ///Each time the Element tree is mounted, the reaction track will be started, in which the association of Observable obtained in the incoming build method (that is, the build attribute of Observer) will be established built = super.build(); ///Call the external incoming build method to build Widget subtree }); ... return built; } @override void unmount() { ///Uninstall Reaction when uninstalling Element reaction.dispose(); super.unmount(); } }
Next, focus on reaction.track
/// ReactionImpl void track(void Function() fn) { _context.startBatch(); ///batch times + 1 _isRunning = true; _context.trackDerivation(this, fn); ///Start tracking the derivation, which is the reaction at this time _isRunning = false; if (_isDisposed) { _context._clearObservables(this); ///Clean up if dispose } if (_context._hasCaughtException(this)) { _reportException(_errorValue._exception); } _context.endBatch(); ///This processing operation is complete }
Enter "context.trackDerivation" method
/// ReactiveContext T trackDerivation<T>(Derivation d, T Function() fn) { final prevDerivation = _startTracking(d); ///Let mainContext start tracking the incoming derivation T result; if (config.disableErrorBoundaries == true) { result = fn(); } else { try { result = fn(); ///Here, call the build function passed in the Observer, which will call the Observable getter method. The derivation mentioned above is the d, so atom will register in the d d._errorValue = null; } on Object catch (e) { d._errorValue = MobXCaughtException(e); } } _endTracking(d, prevDerivation); ///End tracking return result; }
Enter "starttracking"
/// ReactiveContext Derivation _startTracking(Derivation derivation) { final prevDerivation = _state.trackingDerivation; _state.trackingDerivation = derivation; /// the incoming derivation is assigned to the currently being traced, so the getter method of Observable that is called after that is obtained. _resetDerivationState(derivation); ///Reset derivation status derivation._newObservables = {}; ///Empty, convenient to add atom later return prevDerivation; }
Enter "endTracking(d, prevDerivation)"
void _endTracking(Derivation currentDerivation, Derivation prevDerivation) { _state.trackingDerivation = prevDerivation; _bindDependencies(currentDerivation); ///Binding the Observables that derivation depends on }
Enter "bind dependencies (currentderivation)"
void _bindDependencies(Derivation derivation) { final staleObservables = derivation._observables.difference(derivation._newObservables); ///Fetching inconsistent observable sets final newObservables = derivation._newObservables.difference(derivation._observables); ///Take out the new observable collection var lowestNewDerivationState = DerivationState.upToDate; // Add newly found observables for (final observable in newObservables) { observable._addObserver(derivation); ///Key point 1: add this derivation to the Observable observers collection, that is, implement atom holding derivation here // Computed = Observable + Derivation if (observable is Computed) { if (observable._dependenciesState.index > lowestNewDerivationState.index) { lowestNewDerivationState = observable._dependenciesState; } } } // Remove previous observables for (final ob in staleObservables) { ob._removeObserver(derivation); } if (lowestNewDerivationState != DerivationState.upToDate) { derivation .._dependenciesState = lowestNewDerivationState .._onBecomeStale(); } derivation .._observables = derivation._newObservables .._newObservables = {}; // No need for newObservables beyond this point }
As shown in key point 1 above, get the related observable in the derivation and inject the derivation into each observable. So far, the bidirectional binding between the observable and the derivation has been realized
summary
Advantage:
- The components of observer can be updated on demand. Only when the monitored data changes, it will re render
- With Computer computing property mechanism, it will be recycled automatically when there is no reference
- The use of annotations saves template codes such as notify and is concise
- Lower mobx coupling
Disadvantages:
- Too many store s make it impossible to unify data sources. Management is a problem
- There's no time to go back because there's only one reference to the data
- Lack of effective support of middleware mechanism