三羊

三羊的小站

flutter 学习小结(三)

November 30, 2019/「 flutter / Edit on Github ✏️

上一篇文章中,我们学习了 flutter 中三个主要的 widget,分别是 StatelessWidget,StatefulWidget,和 RenderObjectWidget;也简单介绍了 Widget,Element 和 RenderObject 之间的关系。这一篇,将介绍如何在 flutter 应用中进行状态管理以及其基本的原理。

状态管理,实际上就是对数据的管理,这些数据可以是与用户交互的数据,网络请求的数据,或者当前动画的状态数据。在进行状态管理时,需要根据当前状态的影响范围来决定其管理方式。一般是「就近原则」,就是把状态放在距离使用它最近的地方维护。对于组件内部状态(local state),放在组件内部维护就好了;对于多个组件需要共同使用的状态(shared state),就将它放在最近公共祖先中来维护;对于全局状态(global state),一般放在 app 全局状态中维护。现在很多 react 开发者,提到状态管理,就是使用 redux 放在全局管理,也不管这些数据的使用场景。这样做,通常会导致全局状态混乱且不好维护,而且也会造成一些渲染方面的性能问题,是非常不建议的。在 flutter 应用中,基本原则跟 react 是一样的,只不过具体实现方式略有差异而已。

状态管理

local state

对于组件内部的状态,其他地方根本不关心它,也不使用它时,直接将它们放在组件内部维护就好了。这样不仅可以很方便地使用它,也保证了它不被外部污染或者改变。从设计模式上来说,减少了组件的外部依赖,增加组件的内聚性。

在 flutter 中,使用 StatefulWidget 来创建和维护状态。在上一篇「flutter 学习小结(二)」中,详细介绍了 StatefulWidget 和 State,以及其生命周期方法,这里我们通过一个简单的例子来说明如何使用。

class StateDemo extends StatefulWidget {
  
  StateDemoState createState() {
    return StateDemoState();
  }
}

class StateDemoState extends State<StateDemo> {
  /// state
  int _count = 0;

  void _increment() {
    /// setState会对当前组件执行rebuild
    setState(() {
      _count++;
    });
  }

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text('$_count'),
        RaisedButton(
          onPressed: _increment,
          child: Icon(Icons.add),
        ),
      ],
    );
  }
}

在 State 内部有一个_count数据,每次点击按钮,都会将它加 1。

shared state

如果有多个组件需要共享某些状态,那么我们可以将需要共享的状态提升到它们的最近的上层组件中去。

当组件与上层组件是直接父子关系,那么父组件可以将这些状态以及改变状态的方法传递到子组件中。例如,我们可以将上述例子中_count以及_increment直接传递给两个子组件。

/// 上述StateDemoState

Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Child1(_count, _increment),
      Child2(_count),
    ],
  );
}

当组件与上层组件中间有多层组件,它们是祖先与子孙关系时,如果还是通过上面这样层层传递,就会显得十分繁琐,那我们能不能使得子孙组件可以直接获取祖先的数据,并且祖先数据改变了,自动更新子孙组件呢?

react 中是提供了 Context 的方式,将共享数据放在 Context 组件中,它的所有子级组件都可以通过contextType或者Consumer来获得共享数据,具体细节可以查看官方文档。在 flutter 中,可以有如下方案可以选择,

  1. flutter 提供的基础 Widget,包括 InheritedWidget,InheritedModel
  2. flutter 官方提供的库,Provide,
  3. 社区实现的库,包括 Bloc,Redux,Mobx,Scoped Model 等

我们会在下面详细讨论 InheritedWidget,InheritedModel,Provider 的实现,至于社区提供的方案,感兴趣的可以自己去了解。

global state

全局状态是一种特殊的共享状态,它将状态放在 app 根组件中去维护,使得整个 app 的所有子组件都可以获取到,所以它的处理方案跟 share state 中一样。一般的全局状态有 app 主题,语言,或其他用户自定义设置等。下面我们聊聊 flutter 中状态管理的实现原理。

InheritedWidget

我们前面只聊了 flutter 中 StatelessWidget,StatefulWidget,和 RenderObjectWidget。这里,我们来详细聊聊 flutter 另一个比较重要的 Widget,InheritedWidget。

InheritedWidget 的主要作用就是可以直接将数据传递给子树下面的任意子组件,并且当数据变了时,子组件会自动更新。当在一棵 Widget tree 中,子组件可以通过 BuildContext.dependOnInheritedWidgetOfExactType 来获取最近的 InheritedWidget 数据(在 flutter1.12.1 之前是使用 BuildContext.inheritFromWidgetOfExactType)。

/// InheritedWidget源码
abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ Key key, Widget child })
    : super(key: key, child: child);

  
  InheritedElement createElement() => InheritedElement(this);

  
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

InheritedWidget 与其他 Widget 不同之处是,多出了一个updateShouldNotify方法,这个方法需要子类去实现。每当 rebuild 时,则会根据updateShouldNotify返回的值去决定是否需要对依赖它的子组件做更新。下面我们使用 InheritedWidget 来实现上面 count 的例子。

/// 因为Widget是immutable的,所以需要使用StatefulWidget来存储数据
class CountProvider extends StatefulWidget {
  Widget child;

  CountProvider({this.child});

  
  State<CountProvider> createState() {
    return CountProviderState();
  }

  /// 通过CountProvider.of(context)就可以获取InheritedWidget上的数据了
  static ShareDataWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  }
}

/// State用于保存数据,并且更新
class CountProviderState extends State<CountProvider> {
  int _count = 0;

  void _incrementCounter() {
    setState(() {
      _count++;
    });
  }

  
  Widget build(BuildContext context) {
    /// 需要将数据和更新方法传递给InheritedWidget,
    /// 因为子孙组件只能获取到InheritedWidget,而非StatefulWidget和State
    return ShareDataWidget(
      child: this.widget.child,
      count: _count,
      add: _incrementCounter,
    );
  }
}

/// 实现InheritedWidget
class ShareDataWidget extends InheritedWidget {
  final int count;
  final VoidCallback add;
  /// 通过构造函数接受需要共享的数据和方法
  ShareDataWidget({
    Key key,
     Widget child,
    this.count,
    this.add,
  }) : super(child: child, key: key);

  
  bool updateShouldNotify(ShareDataWidget oldWidget) {
    /// 当count不想等时,需要通知子孙组件更新,
    /// 如果count相等,则子孙组件不需要更新
    return oldWidget.count != this.count;
  }
}

如果进一步深究原理,我们需要回答如下 2 个问题,

  1. dependOnInheritedWidgetOfExactType 是如何找到 InheritedWidget 的?
  2. updateShouldNotify 是如何控制子孙组件更新的?

下面,我们来通过分析源码一步一步回答上面的问题。我们知道 context 实际上就是 element,在 element 上调用 dependOnInheritedWidgetOfExactType 就可以取出指定的 InheritedWidget,

  /// Element类
abstract class Element extends DiagnosticableTree implements BuildContext {
  /// 省略其他

  Map<Type, InheritedElement> _inheritedWidgets;
  Set<InheritedElement> _dependencies;

  
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    /// 先实例化一个Set
    _dependencies ??= HashSet<InheritedElement>();
    /// 将祖先InheritedElement加入到_dependencies中
    _dependencies.add(ancestor);
    /// 调用组先updateDependencies,将当前element加入到祖先的_dependencies中
    ancestor.updateDependencies(this, aspect);
    /// 返回祖先InheritedElement关联的widget
    return ancestor.widget;
  }

  
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    /// 先找到祖先InheritedElement
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      /// 注册依赖项,并返回对应的InheritedWidget
      return dependOnInheritedElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
}

它会通过_inheritedWidgets取出对应类型的祖先 InheritedElement,接着会执行如下步骤,

  1. 生成一个 Set 实例,存放依赖项
  2. 将组先 InheritedElement 加入到当前的_dependencies 中
  3. 将当前 element 加入到组先 InheritedElement 的_dependencies 中
  4. 返回祖先 InheritedElement 关联的 Widget

祖先 InheritedElement 和子孙 Element 相互把对方加入到自己的_dependencies 中。那么子孙 Element 是何时将祖先 InheritedElement 加入到自己的_inheritedWidgets的呢?

  /// Element类
abstract class Element extends DiagnosticableTree implements BuildContext {
  /// 省略其他

  void mount() {
    /// ...
    _updateInheritance();
    /// ...
  }

  void activate() {
    /// ...
    _updateInheritance();
    /// ...
  }

  /// 一般element,只是使用parent element的_inheritedWidgets
  /// 但是,InheritedElement会重写_updateInheritance方法
   void _updateInheritance() {
    _inheritedWidgets = _parent?._inheritedWidgets;
  }
}

/// InheritedElement会重写_updateInheritance方法
class InheritedElement extends ProxyElement {
  /// 忽略其他

  
  void _updateInheritance() {
    /// 先取父级element的_inheritedWidgets
    final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();

    /// 最后,将自己加进去
    _inheritedWidgets[widget.runtimeType] = this;
  }
}

看上面代码,逻辑就很简单了,每次 element 在mountactivate会更新_inheritedWidgets。对于普通的 element,_inheritedWidgets就是取的 parent element 的_inheritedWidgets。一直这样迭代,直到碰到InheritedElement,它会将自己加入进去。如果有相同类型的 InheritedElement 的嵌套关系,则会直接覆盖,这也是为什么子孙 element 只能取得离它最近的一个特定类型的 InheritedElement,如下图所示。

inherited

到这里,我们实际上已经回答了第一个问题了。我们再来看看第二个问题,updateShouldNotify 是如何控制子孙组件更新的。

abstract class ProxyElement extends ComponentElement {
  /// 忽略其他的

  
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget;
    /// ...
    /// 会调用InheritedElement的updated
    updated(oldWidget);
    /// ...
  }

  
  void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
  }

  
  void notifyClients(covariant ProxyWidget oldWidget);
}

/// InheritedElement
class InheritedElement extends ProxyElement {
  /// 忽略其他的

  
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    /// 对于StatefulElement,didChangeDependencies方法中会调用State.didChangeDependencies,
    /// 对于其他element,会调用markNeedsBuild,实现rebuild
    dependent.didChangeDependencies();
  }

  /// 重写了Element中updated方法
  
  void updated(InheritedWidget oldWidget) {
    /// 通过updateShouldNotify判断,是否需要更新子孙element
    if (widget.updateShouldNotify(oldWidget))
      /// 再调用ProxyElement的updated,ProxyElement的updated会调用notifyClients
      /// InheritedElement又重写了notifyClients
      super.updated(oldWidget);
  }

  /// 重写了Element中notifyClients方法
  
  void notifyClients(InheritedWidget oldWidget) {
    /// 我们上面说了InheritedElement和子孙element会相互将自己加入到对方的_dependents中
    /// 这里就是循环所有的子孙element,然后调用其didChangeDependencies
    for (Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
}

Element 每次 rebuild,更新其 Widget 之后,都会调用update方法。对于 ProxyElement 而言,它会在update方法里通过调用 InheritedElement 的updated,从而调用 InheritedWidget 的updateShouldNotify来判断是否需要更新子孙 Widget。至此,第二个问题也已尽回答完毕。

InheritedModel

InheritedModel 是 InheritedWidget 的子类,它对 InheritedWidget 做了进一步的封装。当updateShouldNotify返回true时,InheritedWidget 会无条件的对所有依赖它的子组件执行 rebuild。如果我们想当共享的某一部分数据变了,才更新我们的子组件,那么就需要使用 InheritedModel 了。例如,我们共享的数据有 a 和 b,只有当 a 变了,我们才更新子组件 Childa,只有当 b 变了,我们才更新子组件 Childb。如果是用 InheritedWidget 来实现,则不管 a 和 b 哪个变了,Childa 和 Childb 都会更新。

class ABModel extends InheritedModel<String> {
  ABModel({this.a, this.b, Widget child}) : super(child: child);

  final int a;
  final int b;

  /// 跟InheritedWidget中updateShouldNotify一样
  /// 当返回true,然后再根据updateShouldNotifyDependent的返回值,去决定是否需要对子组件执行rebuild
  
  bool updateShouldNotify(ABModel old) {
    return a != old.a || b != old.b;
  }

  /// 比InheritedWidget多了一个判断步骤,
  /// 这个就是根据指定条件去判断,是否需要对子组件执行rebuild
  
  bool updateShouldNotifyDependent(ABModel old, Set<String> aspects) {
    return (a != old.a && aspects.contains('a')) ||
        (b != old.b && aspects.contains('b'));
  }
}

class CountProvider extends StatefulWidget {
  /// ...
  /// 增加aspect参数,
  static ABModel of(BuildContext context, {String aspect}) {
    return InheritedModel.inheritFrom<ABModel>(context, aspect: aspect);
  }
}

class Childa extends StatelessWidget {
  
  Widget build(BuildContext context) {
    /// 只有当a变了,才会对Childa执行rebuild
    ABModel state = CountProvider.of(context, aspect: 'a');
    /// ...
  }
}

class Childb extends StatelessWidget {
  
  Widget build(BuildContext context) {
    /// 只有当b变了,才会对Childb执行rebuild
    ABModel state = CountProvider.of(context, aspect: 'b');
  }
}

接下来,我们从源码实现看看 InheritedModel 是如何做到指定数据变了,才会更新子组件的。

/// InheritedModel继承InheritedWidget
abstract class InheritedModel<T> extends InheritedWidget {
  const InheritedModel({ Key key, Widget child }) : super(key: key, child: child);
  /// 忽略其他的

  
  InheritedModelElement<T> createElement() => InheritedModelElement<T>(this);

  /// Return true if the changes between this model and [oldWidget] match any
  /// of the [dependencies].
  
  bool updateShouldNotifyDependent(covariant InheritedModel<T> oldWidget, Set<T> dependencies);
}

InheritedModel 继承至 InheritedWidget,并提供了updateShouldNotifyDependent方法。它会在widget.updateShouldNotifyDependent返回true时被调用,然后根据其结果,决定是否需要对子组件执行 rebuild。

/// InheritedModelElement也继承InheritedElement
class InheritedModelElement<T> extends InheritedElement {

  /// ...忽略其他的

  /// notifyDependent在updateShouldNotify返回true时才执行
  
  void notifyDependent(InheritedModel<T> oldWidget, Element dependent) {
    final Set<T> dependencies = getDependencies(dependent);
    if (dependencies == null)
      return;
    /// 根据updateShouldNotifyDependent决定是否需要对子组件执行rebuild
    if (dependencies.isEmpty || widget.updateShouldNotifyDependent(oldWidget, dependencies))
      dependent.didChangeDependencies();
  }
}

InheritedWidget 是较底层的能力,InheritedModel 是在它之上做了一些功能增强。如果我们需要自己去实现一些共享状态逻辑处理,建议优先使用 InheritedModel,而非 InheritedWidget。一般情况下,我们可以直接使用 flutter 提供的状态管理库,Provider。

Provider

Provider 的底层实现实际上就是使用的 InheritedWidget,只不过它提供了多种形式的使用方式,比如 Provider,MultiProvider,ProxyProvider,详细使用可以参考官方文档,这里我们说一下它的基本实现。

class Provider<T> {
  /// 忽略其他的

  /// 同样提供了of方法,增加了listen参数,用于控制是否自动更新子组件
  static T of<T>(BuildContext context, {bool listen = true}) {
    // this is required to get generic Type
    final type = _typeOf<InheritedProvider<T>>();
    /// 如果listen为true,则调用inheritFromWidgetOfExactType,它会注册当前InheritedWidget和子孙组件互为依赖项
    /// 否则,使用ancestorInheritedElementForWidgetOfExactType,它只是简单的获取InheritedWidget的数据,而不会注册依赖项
    final provider = listen
      ? context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>
      : context.ancestorInheritedElementForWidgetOfExactType(type)?.widget
        as InheritedProvider<T>;

    if (provider == null) {
      throw ProviderNotFoundError(T, context.widget.runtimeType);
    }

    return provider._value;
  }

    
  Widget build(BuildContext context) {
    /// InheritedProvider就是InheritedWidget的子类
    /// 可以传递自定义的updateShouldNotify
    return InheritedProvider<T>(
      value: delegate.value,
      updateShouldNotify: updateShouldNotify,
      child: child,
    );
  }
}

小结

flutter 中的状态管理,大部分都是依赖 InheritedWidget 来实现的。我们详细列举了 flutter 中常见的几种状态管理场景,以及在跨层共享数据时的具体实现细节,希望本文对你有所帮助,谢谢。

参考

若有收获,小额鼓励