flutter中Mobx的使用及原理

Mobx是什么

mobx的作用可以使用一句话来概括:使用透明的函数响应式编程增强Dart程序中的状态管理,是前端中大名鼎鼎的Mobx.js的Dart版本。

Mobx的三个重要概念

  • Observables: 表示响应式的状态,也可理解为可观察对象。状态指的是应用程序里面的状态或者数据。响应式就是可以感知到、可观察到数据的变化,也就是我们经常接触到的观察者模式。
  • Actions: Actions就是一系列可以引发状态发生变化的动作。
  • Reactions: 状态的观察者,状态发生变化的时候,他们可以收到数据变化的通知。

具体用法

以计数器为例,一个简单的计数器可以表示成一个可观察的数字状态,计数器表示为Counter对象。

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

这里,Mobx需要借助builder_runner这个库生成对应的_$Counter类的代码。
计数器中的值需要通过Flutter_Mobx里面的Widget来反应到UI,使用Observer来包裹需要观察的组件:

final counter = Counter(); // Instantiate the store

Observer(
    builder: (_) => Text('${counter.value}',
    style: Theme.of(context).textTheme.display1,
    ),
),

这里使用了一个叫做Observer的Widget,builder方法中把counter的observable对象的值作为属性传给Text。

官方建议

官方建议,将widget-store-service结合在一起。
Widget:UI,状态的可视化表示
Store: 处理状态
Service: 逻辑操作,包括复杂逻辑,网络请求,本地数据库存储等等。

  1. UI层应该尽量使用StatelessWidget和Observer结合,减少Widget的rebuild次数,提升性能。
  2. Store里面放的 @observable 对象,因为 Dart 在 Flutter 是不能进行运行时反射的,所以复杂对象需要我们自己进行 observable 的声明。否则不会生效。当需要处理衍生状态的时候,可用 computed 替代。

关于不同的页面如何持有Store对象的问题

最简单的是直接写单例的store,但是单例的弊端非常明显,我们需要的是在这几个页面中此对象是同一个,超出这个范围,对象可以销毁,或者使用的是另一个对象,很直接的我们就会需要一个对象管理框架,即依赖注入。
针对这点,官方给出的建议是,可以使用Provider这个框架达到依赖注入的目的,可以查看https://pub.dev/packages/provider

原理

首先,在上面的计数器例子中,看到新生成的类_$Counter,其中部分代码如下:

@override
  int get value {
    _$valueAtom.reportRead();
    return super.value;
  }

  @override
  set value(int value) {
    _$valueAtom.reportWrite(value, super.value, () {
      super.value = value;
    });
  }

可以看到在获取变量时,会调用reportRead(), 设置变量会调用reportWrite
我们先看看reportRead()做了什么,

void reportRead() {
    context.enforceReadPolicy(this);//1
    reportObserved();//2
  }

第一行代码是为了确保观察值在Actions和Reactions内部读取。重点在于第二行调用的reportObserved方法,其代码如下:

//atom可以理解为对应的被观察对象的封装
void _reportObserved(Atom atom) {
    final derivation = _state.trackingDerivation;

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

可以看出来这段代码做的事情就是:把当前的变量加入到被观察的队列中去,如果变量未被观察,则把此变量设置为观察状态。
再来看看reportWrite做了什么:

void reportWrite<T>(T newValue, T oldValue, void Function() setNewValue) {
    context.spyReport(ObservableValueSpyEvent(this,
        newValue: newValue, oldValue: oldValue, name: name));

    // ignore: cascade_invocations
    context.conditionallyRunInAction(() {
      setNewValue();
      reportChanged();
    }, this, name: '${name}_set');

    // ignore: cascade_invocations
    context.spyReport(EndedSpyEvent(type: 'observable', name: name));
  }

其核心是执行了reportChanged方法,

  void reportChanged() {
    _context
      ..startBatch()
      ..propagateChanged(this)
      ..endBatch();
  }

来看看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;
    }
  }

当数据需要更新的时候,调用观察者的_onBecomeState方法。查看observable.dart并追踪下去(过程过于复杂,这里不多赘述),可以发现最终就是调用了reaction的run方法,

for (final reaction in remainingReactions) {
        reaction._run();
      }

如下:


  @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();
  }

其中的_onInvalidate()就是在observer构成的时候传入的方法:

  void _invalidate() => setState(noOp);

  static void noOp() {}

看到这里,我们发现就是通过调用setState从而刷新了widget。

参考资料

  1. https://juejin.cn/post/6844903860184563720
  2. https://takeroro.github.io/2020/06/30/mobX%20flutter%20%E6%95%B0%E6%8D%AE%E6%B5%81%E5%8A%A8/
  3. https://cn.mobx.js.org/
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容