统一状态管理

102 阅读10分钟

State对象只负责管理自己的状态,而应用显然不会只有一个组件,因此这种方案一定不适用于开发一个完整的Flutter应用,我们需要的是一个能连接多个组件并且有效管理它们内部状态的方案。

想要在多个组件间通信,传统做法就是通过构造函数传递属性实现。我们可以将状态保存在根组件中并统一管理,并且通过逐层传递的方式将某些状态值传递到需要它们的组件中。

image.png

采用这种方式可以做到统一状态管理。顶层组件想要更新状态则通过组件的参数一层一层的向下传递。如果最底层想要更新界面则需要通过一层一层的回调函数告诉顶层组件状态已更新刷新页面。这种方案维护起来非常困难,编码也十分麻烦,尤其是需要在路由间共享状态时,必须手动重写多段重复的代码。当应用有超过一个页面或者其中多个组件共享状态时,就可以放弃使用这种状态管理方案了。

幸运的是Google为我们提供了一个可遗传组件。IneritedWidget,从名称就可以看出它的功能,继承它的任意子组件都具有它的内部状态。需要顶层数据的组件可以直接通过某种方式获得底层InheritedWidget中的状态数据。

image.png InheritedWidget是Flutter中除了无状态组件和有状态组件之外的第三类组件,它是一个抽象类,我们可以继承它,实现这种遗传数据的功能。Flutter在源代码中使用InheritedWidget实现了很多重要的功能,如Theme、MediaQuery等。

InheritedWidget 实现机制

分析:

  1. 子widget是如何获取从当前widget到根widget路径中的所有InheritedWidget实例
  2. InheritedWidget 是如何做到局部刷新的
  3. 在实现的第一种方式中,为什么在InheritedWidget外部需要包一层父widget才可以实现局部刷新
  4. 系统提供的InheritedNotifier是如何在不包裹父widget实现 InheritedWidget局部刷新机制的

问题一分析

flutter视图的创建过程大致为:

  1. 创建widget
  2. 调用widget 的createElement() 方法创建Element
  3. 调用Element的mount()方法
  4. 在Element的mount方法中,遍历子widget,创建和其对应的Element然后调用其mount()方法

flutter通过上述流程递归完成整个Element树的创建和挂载. 我们可以看到Element的挂载方法mount(),是Element树创建的核心方法也是其必调的方法,而Element代码规定其所有子类也必须调用 父类的挂载方法mount(),如下为mount的简化方法

  // @mustCallSuper 标明子类必须调用父类方法
  @mustCallSuper
  void mount(Element? parent, Object? newSlot) {
    ....
    //InheritedWidget数据传递 的核心实现.
    _updateInheritance();
    ...
  }
复制代码

每个widget所对应的Element 在被添加到Element树中时(调用mount()方法),都会调用updateInheritance()方法,而其实现如下:

 Map<Type, InheritedElement>? _inheritedWidgets;
void _updateInheritance() {
    //将父Element _parent的 _inheritedWidgets实例赋值给子类
    _inheritedWidgets = _parent?._inheritedWidgets;
  }
复制代码

每个Element都会有一个 Map<Type, InheritedElement>引用指向父类的Map<Type, InheritedElement>,也就是说:没有重写_updateInheritance方法的子Element和父Element共享同一个 Map<Type, InheritedElement>对象,其内部存储:InheritedWidget对应的InheritedElement的类型和其实例,
上面的_updateInheritance方法只有赋值引用,但是并没有创建Map<Type, InheritedElement>实例, 那么是什么地方创建的呢 在整个Element体系中,只有InheritedWidget对应的InheritedElement重写了_updateInheritance方法,并创建相应实例,如下:

 @override
  void _updateInheritance() {
    final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
    //如果父类incomingWidgets 不为空,则将父类数据拷贝进一个新创建的HashMap实例
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    _inheritedWidgets![widget.runtimeType] = this;
  }
复制代码

即: InheritedElement 在父类Map<Type, InheritedElement>的基础上 创建新的Map,并将自身也添加到该实例中,

结论: 所有InheritedElement的子Element共享同一个Map<Type, InheritedElement>实例,而且每一个子Element都持有该引用.

我们回头看,子widget获取InheritedWidget数据的实现:

  static T? ofData<T>(BuildContext context) {
    //获取DataInheritedWidget对应的Elemnt实例,
    return (context
            .getElementForInheritedWidgetOfExactType<DataInheritedWidget<T>>()
            ?.widget as DataInheritedWidget<T>?)
        ?.data;
  }

  static T? of<T>(BuildContext context) {
    //获取DataInheritedWidget对应的Elemnt实例后并对DataInheritedWidget进行依赖监听
    return context
        .dependOnInheritedWidgetOfExactType<DataInheritedWidget<T>>()
        ?.data;
  }
复制代码

上述方法都是在子widget中调用的,也就是context是子widget实例中的context,我们知道widget中的BuildContext就是Element, 上述方法在Element中的实现如下:

  @override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

  @override
  InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    return ancestor;
  }
复制代码

我们先看,仅获取数据而不依赖监听的 getElementForInheritedWidgetOfExactType方法,其内部实现为: final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];

而: _inheritedWidgets即是:前面分析的 InheritedElement所有子Element所共享的 Map<Type, InheritedElement>? _inheritedWidgets,

这里也就是解决了第一个疑问, 子widget是如何获取从当前widget到根widget路径中的所有InheritedWidget : 即是:(1)子widget内部对应的Element持有一个 缓存从当前节点到根节点路径上的所有InheritedElement的map引用: Map<Type, InheritedElement>? _inheritedWidgets (2)我们可以通过调用Elemnt的widget方法获取和其对应的Widget实例

问题二、三 分析

问题一中getElementForInheritedWidgetOfExactType是仅获取InheritedWidget的数据而不对InheritedWidget进行依赖监听, 而dependOnInheritedWidgetOfExactType 是获取InheritedWidget数据后并对InheritedWidget进行依赖监听,对比两个方法实现中: dependOnInheritedWidgetOfExactType getElementForInheritedWidgetOfExactType多调用了一行dependOnInheritedElement(ancestor, aspect: aspect) ,

dependOnInheritedElement(ancestor, aspect: aspect) 的实现如下:

abstract class Element {
 ....
  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
    assert(ancestor != null);
    //如果dependencies为null 则初始化
    _dependencies ??= HashSet<InheritedElement>();
    //将依赖对象增加到依赖对象合集中
    _dependencies!.add(ancestor);
    //更新InheritedElement的被依赖对象集合
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
....
}
复制代码

当我们调用dependOnInheritedWidgetOfExactType 方法时,我们会找到相对应的InheritedElement, 将其添加到自己的依赖集合中,并将自己添加到InheritedElement的被依赖合集了,当InheritedWidget进行更新时,便能定位到所有依赖自己的子widget,

为了分析InheritedWidget的局部刷新,我们需要先大致了解下widget的刷新机制

  1. 调用Element->markNeedsBuild 将自己标记为脏数据 即_dirty = true,然后调用BuildOwner->scheduleBuildFor(Element element)方法
  2. 在scheduleBuildFor方法做了两件事,将当前需要更新的widget的Element添加进 List<Element> _dirtyElements 集合中并调用刷新屏幕的方法 scheduleFrame()
  3. 当下一帧刷新到来时 WidgetsBinding->drawFrame(),然后BuildOwner->buildScope(Element context, [ VoidCallback? callback ]),
  4. 在buildScope的内部,遍历_dirtyElements集合,并调用 _dirtyElements[index].rebuild();
  5. Element->rebuild() => Element->performRebuild()

这里是widget刷新的大致流程,针对不同的Element,其performRebuild方法会有区别,

InheritedWidget对应的InheritedElement 对上的继承关系为 InheritedElement ->ProxyElement ->ComponentElement -> Element ,整个继承链中,只有ComponentElement重写了 performRebuild()方法,去除无用代码后如下:

  void performRebuild() {
    Widget? built;
    try {
      //创建Widget
      built = build();
    } catch (e, stack) {
    } finally {
      _dirty = false;
    }
    try {
      //根据新创建的widget 更新 Element? _child;
      _child = updateChild(_child, built, slot);
    } catch (e, stack) {
      _child = updateChild(null, built, slot);
    }
   }
复制代码

而InheritedElement的父类ProxyElement 重写了build()方法

Widget build() => widget.child;
复制代码

即,创建的widget,是传递给ProxyWidget 的子widget参数, InheritedElement继承 ProxyElement但是没有重写build(),所以其方法也是如此,(如果子widget实例不变,build()返回的对象永远相同)

对于updateChild()更新方法, InheritedElement的继承链都没重写,下面是Element类的updateChild方法:

 Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    final Element newChild;
    if (child != null) {
      //在刷新时 child不为空,所以走此处
      bool hasSameSuperclass = true;
      if (hasSameSuperclass && child.widget == newWidget) {
        //1,注意此处,如果新创建的子widget和之前持有的子Element的widget相同则不更新
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        //2,如果两个widget的runtimeType和key不同,则进行更新
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        newChild = child;
      } else {
        //如果上述情况都不满足,则重新构建widget和element,并进行挂载
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }
    return newChild;
    }
复制代码

注意上述 1 处,当InheritedWidget自身进行更新时,其刷新流程是:

  1. InheritedElemnt->rebuild()
  2. InheritedElemnt->performRebuild()
  3. InheritedElemnt->updateChild()

在performRebuild方法中 如我们上述所说,InheritedElemnt调用 Widget build() => widget.child; 返回的外部传递的widget,所以如果更新流程是从InheritedWidget自身开始且传递的子widget不变,则build()返回的值恒定. => 在updateChild()方法中触发上述 1 处标明的条件,即其子Widget不会更新,(如果传递更新节点比较高,导致传递进来的widget对象发生变化,则会像正常的widget一样刷新)

上述,我们得到一个结论,如果仅调用InheritedElement的更新方法,进行更新时,其内部会因为返回的widget实例相同,而不更新子widget

那么当我们在InheritedWidget外部包装一个(非ProxyWidget类型)父widget,并调用其内部的setState方法时, 因为build() 返回的widget实例,是新创建的,所以和当前Element的widget不同,所以会走 2 处, 调用子Element的update(covariant Widget newWidget) 方法

在InheritedElement的继承链中 ProxyElement 重写了该方法

 @override
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget;
    super.update(newWidget);
    //调用ProxyElement-> updated()
    updated(oldWidget);
    _dirty = true;
    //调用父类Element的rebuild方法 => Element->performRebuild()=>ComponentElement-> performRebuild()=>回到上述我们分析的InheritedElement的更新流程
    rebuild();
  }
复制代码

所以当 InheritedWidget的父widget 调用其 子Element(即是InheritedElement)的update方法最终调用了 ProxyElement-> updated()方法

ProxyElement-> updated()方法详情

 @protected
  void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
  }
  @protected
  void notifyClients(covariant ProxyWidget oldWidget);
复制代码

InheritedElement分别重写了这两个方法

  @override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
  }
  @override
  void notifyClients(InheritedWidget oldWidget) {
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
  
   @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }
复制代码

可以看到

  1. InheritedElement->updated()=>
  2. InheritedElement->notifyClients()=>
  3. InheritedElement->notifyDependent()=>
  4. Element-> didChangeDependencies

我们在前面分析问题一的时候,当调用 dependOnInheritedElement方法时,会将当前的Element填充进InheritedWidget 的dependents集合中,所以InheritedWidget再notifyClients方法中遍历其被依赖的集合并调用其didChangeDependencies方法

而Element的didChangeDependencies方法 会调用markNeedsBuild方法 更新自己

  @mustCallSuper
  void didChangeDependencies() {
    markNeedsBuild();
  }
复制代码

结论:经过以上分析,得到第二第三个问题结论: 如果仅更新InheritedWidget自身的话,因为传递给InheritedWidget的子widget实例不变,所以在更新子widget的过程,创建的widget实例都相同,所以不刷新子widget, 同时因为从子节点开始更新的话 不会调用的 void update() 方法.所以也不会更新InheritedWidget被监听依赖的Widget集合

当InheritedWidget的父widget的 调用进行刷新时,调用child.update() 方法,即是InheritedWidget会调用notifyClients 遍历其被依赖的集合Element,调用其didChangeDependencies方法进行更新,而InheritedWidget本身因为build()方返回的widget实例是同一个,故其本身不刷新, 即是实现了局部刷新

问题四分析

经过问题二三的分析,我们知道InheritedWidget 本身刷新时,因为没有调用notifyClients方法,所以被依赖的对象没有进行刷新,那如果我们手动调用 是不是就实现了 InheritedWidget的自动刷新机制呢 ,

官方的InheritedNotifier就是通过手动调用notifyClients方法来实现更新的

InheritedNotifierElement 手动调用notifyClients方法

class _InheritedNotifierElement<T extends Listenable> extends InheritedElement {
  _InheritedNotifierElement(InheritedNotifier<T> widget) : super(widget) {
    widget.notifier?.addListener(_handleUpdate);
  }
  @override
  InheritedNotifier<T> get widget => super.widget as InheritedNotifier<T>;

  bool _dirty = false;

  @override
  void update(InheritedNotifier<T> newWidget) {
    final T? oldNotifier = widget.notifier;
    final T? newNotifier = newWidget.notifier;
    if (oldNotifier != newNotifier) {
      oldNotifier?.removeListener(_handleUpdate);
      newNotifier?.addListener(_handleUpdate);
    }
    super.update(newWidget);
  }

  @override
  Widget build() {
    //手动调用
    if (_dirty)
      notifyClients(widget);
    return super.build();
  }

  void _handleUpdate() {
    _dirty = true;
    markNeedsBuild();
  }

  @override
  void notifyClients(InheritedNotifier<T> oldWidget) {
    super.notifyClients(oldWidget);
    _dirty = false;
  }

  @override
  void unmount() {
    widget.notifier?.removeListener(_handleUpdate);
    super.unmount();
  }
}

复制代码

总结:

1. 子widget内部对应的Element持有一个 缓存从当前节点到根节点路径上的所有InheritedElement的map引用: Map<Type, InheritedElement>? _inheritedWidgets Element可以通过Type获取相应的InheritedElement实例进而获取对应的InheritedWidget

2. Element在调用dependOnInheritedWidgetOfExactType方法时,会将自己添加进InheritedElement的Map<Element, Object?> _dependents集合中,

3. InheritedElement刷新调用updateChild()方法时,因为build()返回的widget实例不变,所以导致不会调用 Elemnt->update()方法,既:不会更新子widget

4. 当InheritedElement的父Elemnt 刷新执行 child.update(newWidget),方法会调用 InheritedElement->updated() => InheritedElement->notifyClients(),在notifyClients的方法中会遍历 _dependents 集合并调用 Element->didChangeDependencies()方法,而didChangeDependencies方法中会调用 markNeedsBuild()方法,进而实现了InheritedWidget被依赖对象的刷新

综上 InheritedWidget实现了 子widget可以获取父InheritedWidget的实例,并实现了局部刷新