The way of Flutter state management

Posted by Meltdown on Sun, 19 Jan 2020 09:59:53 +0100

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

  1. 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++;
  }
}
  1. 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:

  1. _$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
      }
    }
  }
  1. _$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

  1. _$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

  1. 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

  1. 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:

  1. The components of observer can be updated on demand. Only when the monitored data changes, it will re render
  2. With Computer computing property mechanism, it will be recycled automatically when there is no reference
  3. The use of annotations saves template codes such as notify and is concise
  4. Lower mobx coupling

Disadvantages:

  1. Too many store s make it impossible to unify data sources. Management is a problem
  2. There's no time to go back because there's only one reference to the data
  3. Lack of effective support of middleware mechanism
Published 14 original articles, won praise 11, visited 10000+
Private letter follow

Topics: Attribute REST