navigation

  • Study Part I — Simple Use of Dialog
  • Study part ii — Drawer and Water pattern effect

preface

MobX is a popular form of functional responsive programming on the front end that makes state simple and extensible. The philosophy behind it is:

Anything that comes from the state of the application should be acquired automatically

The observer-based MVVM framework completes the bidirectional binding of data to the UI. Google released ViewModel, a similarly minded MVVM framework, in 2017. MVVM is a framework for data-driven updates that makes it easy to pull pages and logic away and is popular on the front end. So MobX came out with a version of the Dart to support the Flutter. Let’s start by introducing MobX into our Flutter.

use

First release the official website, use several steps:

1. First, introduce dependency:


  mobx: ^ 0.2.0
  flutter_mobx: ^ 0.2.0
  mobx_codegen: ^ 0.2.0

Copy the code

2. Add a store:


import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';

// Automatically generated classes
part 'settings_store.g.dart';
class SettingsStore = _SettingsStore with _$SettingsStore;

abstract class _SettingsStore implements Store {
  var key = {
    "showPage":"showPage"};@observable
  String showPage = "";

  @action
  getPrefsData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    showPage = prefs.get(key["showPage"])??"Home page";
  }

  @action
  saveShowPage(String showPage) async {
    if(showPage == null) {
      return;
    }
    this.showPage = showPage;
    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setString(key["showPage"], showPage); }}Copy the code

For the Dart version of Mobx, bidirectional binding is achieved by generating new classes, so you need to add some definitions of generated classes in the Store:

part 'settings_store.g.dart';
class SettingsStore = _SettingsStore with _$SettingsStore;
Copy the code

$SettingsStore is the class to be generated, and SettingsStore is a new class that mixes the two stores. Here are the automatically generated classes:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'settings_store.dart';

/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// StoreGenerator
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars

mixin _$SettingsStore on _SettingsStore, Store {
  final _$showPageAtom = Atom(name: '_SettingsStore.showPage');

  @override
  String get showPage {
    _$showPageAtom.reportObserved();
    return super.showPage;
  }

  @override
  set showPage(String value) {
    _$showPageAtom.context.checkIfStateModificationsAreAllowed(_$showPageAtom);
    super.showPage = value;
    _$showPageAtom.reportChanged();
  }

  final _$getPrefsDataAsyncAction = AsyncAction('getPrefsData');

  @override
  Future getPrefsData() {
    return _$getPrefsDataAsyncAction.run(() => super.getPrefsData());
  }

  final _$saveShowPageAsyncAction = AsyncAction('saveShowPage');

  @override
  Future saveShowPage(String showPage) {
    return _$saveShowPageAsyncAction.run(() => super.saveShowPage(showPage)); }}Copy the code

There are several steps to achieve the above effect:

  • Add @Observable annotation to data that needs to be observed, @Action annotation to methods that need to perform operations,

  • Next, execute the flutter packages pub run build_runner build

  • In particular, if you want to keep track of store changes in real time to change the newly generated class in real time, you need to execute a command:

    Flutter packages pub run build_runner watch, if the operation fails, try the clean command below:

    flutter packages pub run build_runner watch --delete-conflicting-outputs

3. Use in widgets:

Add a layer of Observer widgets to the widgets that need to watch the data change,


_buildShowPageLine(BuildContext context) {
    return GestureDetector(
        onTap: () {
          showDialog<String>(
              context: context,
              builder: (context) {
                String selectValue = '${settingsStore.showPage}';
                List<String> valueList = ["Home page"."Life"];
                return RadioAlertDialog(title: "Select display page",
                    selectValue: selectValue,
                    valueList: valueList);
              }).then((value) {
                print(value);
                settingsStore.saveShowPage(value);
          });
        },
        
        // Add a layer of Observer widgets to the widgets that need to observe changes.
        child: Observer(
            builder: (_) => ListTile(
                  title: Common.primaryTitle(content: "Default display page"),
                  subtitle: Common.primarySubTitle(content: '${settingsStore.showPage}')))); }Copy the code

Complete the above steps to automatically refresh the widget by manipulating the store’s data.

The principle of

After reading the above usage, I believe the reader will feel confused and magical. Don’t worry, let’s go into the principles. First look at the newly generated code _$SettingsStore, which has several key pin codes,


 @override
  String get showPage {
    _$showPageAtom.reportObserved();
    return super.showPage;
  }

  @override
  set showPage(String value) {
    _$showPageAtom.context.checkIfStateModificationsAreAllowed(_$showPageAtom);
    super.showPage = value;
    _$showPageAtom.reportChanged();
  }

Copy the code

It can be seen that when obtaining a variable, dart reportObserved() is called, and when setting a variable, DART reportChanged is called. From the name, it can be seen that obtaining a variable is to report the variable to the observed state, while setting a variable is to report data changes for notification. Let’s first look at what reportObserved() does,

// Atom can be understood as a wrapper around the corresponding observed variable
  void _reportObserved(Atom atom) {
    final derivation = _state.trackingDerivation;

    if(derivation ! =null) {
      derivation._newObservables.add(atom);
      if(! atom._isBeingObserved) { atom .. _isBeingObserved =true. _notifyOnBecomeObserved(); }}}Copy the code

You can see that the core is to add the current variable to the queue being observed.

What did reportChanged do,


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; }}Copy the code

The key code is

if (observer._dependenciesState == DerivationState.upToDate) {
    observer._onBecomeStale();
 }
Copy the code

When the data needs to be updated, the observer’s _onBecomeStale method is called, and the intelligent reader will remember that the observer exists. That’s where we use the Observer widget on top of the observed data widget. The source code is as follows:


library flutter_mobx;

// ignore_for_file:implementation_imports
import 'package:flutter/widgets.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx/src/core.dart' show ReactionImpl;

/// Observer observes the observables used in the `builder` function and rebuilds the Widget
/// whenever any of them change. There is no need to do any other wiring besides simply referencing
/// the required observables.
///
/// Internally, [Observer] uses a `Reaction` around the `builder` function. If your `builder` function does not contain
/// any observables, [Observer] will throw an [AssertionError]. This is a debug-time hint to let you know that you are not observing any observables.
class Observer extends StatefulWidget {
  /// Returns a widget that rebuilds every time an observable referenced in the
  /// [builder] function is altered.
  ///
  /// The [builder] argument must not be null. Use the [context] to specify a ReactiveContext other than the `mainContext`.
  /// Normally there is no need to change this. [name] can be used to give a debug-friendly identifier.
  const Observer({@required this.builder, Key key, this.context, this.name})
      : assert(builder ! =null),
        super(key: key);

  final String name;
  final ReactiveContext context;
  final WidgetBuilder builder;

  @visibleForTesting
  Reaction createReaction(Function() onInvalidate) {
    final ctx = context ?? mainContext;
    return ReactionImpl(ctx, onInvalidate,
        name: name ?? 'Observer@${ctx.nextId}');
  }

  @override
  State<Observer> createState() => _ObserverState();

  void log(Stringmsg) { debugPrint(msg); }}class _ObserverState extends State<Observer> {
  ReactionImpl _reaction;

  @override
  void initState() {
    super.initState();

    _reaction = widget.createReaction(_invalidate);
  }

  void _invalidate() => setState(noOp);

  static void noOp() {}

  @override
  Widget build(BuildContext context) {
    Widget built;
    dynamic error;

    _reaction.track(() {
      try {
        built = widget.builder(context);
      } on Object catch(ex) { error = ex; }});if(! _reaction.hasObservables) { widget.log('There are no observables detected in the builder function for ${_reaction.name}');
    }

    if(error ! =null) {
      throw error;
    }
    return built;
  }

  @override
  void dispose() {
    _reaction.dispose();
    super.dispose(); }}Copy the code

What we see here is that the Observer inherits from StatefulWidget. We have a parent widget on top of our widget that is StatefulWidget type, so as soon as we update the parent widget, In the same way, our widget can be updated. During the build process, you can see that the track method is called. Tracing the source code shows that the incoming method is called first (in this case, the corresponding method is our widget build), and then the Observer is inserted into the Observer queue:

void _bindDependencies(Derivation derivation) {
    final staleObservables =
        derivation._observables.difference(derivation._newObservables);
    final newObservables =
        derivation._newObservables.difference(derivation._observables);
    var lowestNewDerivationState = DerivationState.upToDate;

    // Add newly found observables
    for (final observable in newObservables) {
      observable._addObserver(derivation);

      // 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
  }

Copy the code

Then we need to find the observer’s _onBecomeStale method. If we trace the _onBecomeStale method, we can see that reaction’s run method is finally called:


  @override
  void _run() {
    if (_isDisposed) {
      return;
    }

    _context.startBatch();

    _isScheduled = false;

    if (_context._shouldCompute(this)) {
      try {
        _onInvalidate();
      } on Object catch (e) {
        // Note: "on Object" accounts for both Error and Exception
        _errorValue = MobXCaughtException(e);
        _reportException(e);
      }
    }

    _context.endBatch();
  }

Copy the code

The _onInvalidate() method is passed in when the observer is constructed:


  void _invalidate() => setState(noOp);

  static void noOp() {}

Copy the code

Now, it’s clear that you’re refreshing the widget by calling the setState.

supplement

A reader asked how Mobx handles annotations because Flutter does not support reflection. For this reason, I purposely went through the source code of Mobx, and found that Mobx is the TypeChecker class that uses the static type checking of DART.


/// An abstraction around doing static type checking at compile/build time.
abstract class TypeChecker {
  const TypeChecker._();
}
Copy the code

We know that the Debug mode of the flutter is JIT compiled, so the runtime information is available, while the Product mode is AOT compiled, so the Runtime information is not available. So in the Dart version of Mobx, it actually generates a template code in debug mode to support it. In this way, Mobx can support reflection features when simulating Runtime.

conclusion

For Mobx, the essence is to have a parent widget on top of the widget that uses the observed data. The parent widget is a StatefulWidget. Through the Observer pattern, the Observer is notified when data changes are detected, and the Observer calls setState, updating the Observer, and finally refreshing the sub-widget.

warehouse

Click on flutter_demo to see the full code.