InheritedWidget 源码分析和应用

855 阅读15分钟

目前,开发的flutter项目中,状态管理库使用是ProviderProvider 基于 InheritedWidget 组件封装,想要减少日常开发采坑,就不得不去了解 InheritedWidget 组件的工作原理。由于要从源码角度分析 InheritedWidget 组件的工作原理,在阅读本文前,最好对 flutter 的知识有一定了解,这样才能更好的了解,本文所要表达的意思。

  • 熟悉 flutter 基本使用。
  • 了解 provider 的框架。
  • 了解 WidgetElement 之间关系。
  • 了解 Elementflutter 渲染时方法的调用。

一、 InheritedWidget

本文中用到的生产环境

生产环境

1.1 InheritedWidget 简述

/// 有效地沿树向下传播信息的 Widget 的基类,子 Widget 要想获取最近特定类型的 InheritedWidget实例,请使用 
/// [BuildContext.dependOnInheritedWidgetOfExactType]。子 Widget 以这种方式引用时,当 InheritedWidget 改变状态
/// 时,会重新构建依赖的子 Widget。
abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ Key key, Widget child })
      : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);

InheritedWidget 代码很简单,主要有两个方法:

  • createElement 生成 InheritedElement 对象。
  • updateShouldNotify 用于控制当前 InheritedWidget 发生变化, 所依赖的 Widget 是否需要重建。
1.1.1 BuildContext

InheritedWidget 描述可得知,如果子 Widget 需要获取 InheritedWidget 对象,可以通过 BuildContext.dependOnInheritedWidgetOfExactType 获取。看下 BuildContext 类的 dependOnInheritedWidgetOfExactType

abstract class BuildContext {
  /// 获取给定类型“T”的最近 widget,它必须是具体 [InheritedWidget] 子类的类型,并将此构建上下文注册到该 widget,以便当该 widget 更改时(或引入该类型的新 widget, 或 widget 消失),此构建上下文将被重建,以便它可以从该 widget 获取新值。
   T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect });
  
  /// 获取与给定类型“T”的最近 widget 对应的 element,该 element 必须是具体 [InheritedWidget] 子类的类型。 如果找不到这样的 element,则返回 null。     
  /// 调用这个方法是 O(1) 一个小的常数因子。 此方法不会像 [dependOnInheritedWidgetOfExactType] 那样与目标建立关
  /// 系。 不应从 [State.dispose] 调用此方法,因为此时 element 树不再稳定。 要从该方法引用祖先,请通过在 
  /// [State.didChangeDependencies] 中调用 [dependOnInheritedWidgetOfExactType] 来保存对祖先的引用。 使用 
  /// [State.deactivate] 中的此方法是安全的,每当 widget 从树中移除时都会调用该方法。
  InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();
}

1.2 InheritedWidget 应用

接下来,我们写一个基于 InheritedWidget 组件实现的 计数器

1.2.1 CountScope
class CountScope extends InheritedWidget {
  const CountScope({this.count, Widget child}) : super(child: child);

  final int count;

  static CountScope of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CountScope>();
  }

  @override
  bool updateShouldNotify(CountScope oldWidget) {
    return oldWidget.count != count;
  }
}

CountScope 继承 InheritedWidget

  1. of ,子 Widget 获取 CountScope 对象。[见1.1小节]
  2. updateShouldNotifyoldWidget.count != count 刷新依赖的 widget[见1.1小节]
1.2.2 CountWidget
class CountWidget extends StatefulWidget {
  const CountWidget({Key key}) : super(key: key);

  @override
  _CountWidgetState createState() => _CountWidgetState();
}

class _CountWidgetState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print('_CountWidgetState build');
    final int count = CountScope.watch(context).count;
    return Container(child: Text('$count', style: Theme.of(context).textTheme.headline4));
  }
}

CountWidget 调用 CountScope.of(context).count 显示计数结果。

1.2.3 MyHomePage
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountScope(count: _counter, child: CountWidget()),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

到这里计数器功能差不多就完成了,当我们点击浮动 + 按钮时,计数器的数值会累加,最后可以看到效果就是这样。

1.2.4 updateShouldNotify

我们在 1.1 小节 提过, updateShouldNotify 返回 true ,表示更新依赖的 Widgetfalse 不更新,现在让我们验证下。修改 CountScope 代码,当 count < 3,才能更新依赖的 widget

class CountScope extends InheritedWidget {
  const CountScope({this.count, Widget child}) : super(child: child);

  final int count;

  static CountScope watch(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CountScope>();
  }

  @override
  bool updateShouldNotify(CountScope oldWidget) {
     //return oldWidget.count != count;
    print('count:$count');
    return count < 3;
  }
}

结果确实如我们所料那样,不停的点击 + 按钮,界面上的数字一直显示的是 2。认真观察 log 会发现,实际上 count 是会一直累加。

3.png

讲到这里,我们就会有很多疑问?为什么 CountWidget 能获取到 CountScopecount 的值?又为什么CountScopecount 数值有变化,但是当 count > 3 时,CountWidget 界面却没有更新呢?

遇事不决,看源码

二、Widget 更新流程

我们知道调用 setState 后,把当前 element 标记为 dirty, 当下一次 vsync 信号到来的时候,回调执行 handleBeginFramehandleDrawFrame ,然后经过一些列的调用,最后会调用 BuildOwner.buildScope ,遍历 _dirtyElements 集合,调用 Elementrebuild 刷新组件。

本文不会完整介绍 Widget 的更新机制,有兴趣的同学,可以自己去了解下。(接下来源码片段,都只保留关键代码)。

2.1 类图

4.png

2.2 时序图

5.png

2.3 Element#rebuild

前面,我们提到调用 setState 后 ,经过一系列的调用,最终调用 Elementrebuild

abstract class Element extends DiagnosticableTree implements BuildContext {
  /// 当调用 [BuildOwner.scheduleBuildFor] 会将该 Element 标记为 dirty,
  /// 当 Element 首次 build 时会被 [mount] 调用,当 widget 更改时由 [update] 调用。
  void rebuild() {
    if (!_active || !_dirty)
      return;
    performRebuild();
  }

  /// 在进行适当的检查后由 rebuild() 调用。
  @protected
  void performRebuild(); /// [见2.4小节]
}

2.4 ComponentElement#performRebuild

abstract class ComponentElement extends Element {

  /// 调用 StatelessWidget 对象的 StatelessWidget.build 方法(对于无状态小部件)
  /// 或 State 对象的 State.build 方法(对于有状态小部件),然后更新 widget 树。
  /// 在 mount 期间自动调用以生成第一个构建,并在 element 需要更新时通过 rebuild 调用。
  @override
  void performRebuild() {
    Widget built;
    try {
      built = build();  
    } finally {
      /// ...
      _dirty = false;
    }
    try {
      _child = updateChild(_child, built, slot); /// [见2.5小节]
    } catch (e, stack) {
      // ...
      _child = updateChild(null, built, slot);
    }

  /// 子类应该覆盖这个函数来为它们的小部件实际调用适当的 `build` 函数
  ///(例如,[StatelessWidget.build] 或 [State.build])。
  @protected
  Widget build();
}

2.5 Element#updateChild

abstract class Element extends DiagnosticableTree implements BuildContext {

  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget); /// [见2.6小节]
        newChild = child;
      } else {
        deactivateChild(child);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else { 
      newChild = inflateWidget(newWidget, newSlot); /// [见2.5.1小节]
    }
    return newChild;
  }
}
2.5.1 Element#inflateWidget

如果第一次调用 updateChild,默认 child = null,就会执行 inflateWidget,生成 Element newChild 对象,最后把 newChild 赋值给 _child [2.4小节],后续就使用 newChild 传入 updateChild

	Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        // ...
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        return updatedChild;
      }
    }
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);// [见2.5.2小节]
    return newChild;
  }

调用 widget.createElement,生成 Element 对象,然后调用 Elementmount,递归调用,完成所有子 widget 的刷新。

2.5.2 Element#mount
  void mount(Element parent, dynamic newSlot) {
    //...
    final Key key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    _updateInheritance();
  }
  void _updateInheritance() {
    _inheritedWidgets = _parent?._inheritedWidgets;
  }

将父类中的 _inheritedWidgets 集合对象,传到子类。

2.6 Element#update

[2.5.1] [2.5.2] 两个小节,描述第一次加载 widget 过程。接下来介绍 child.update(newWidget)

  @mustCallSuper
  void update(covariant Widget newWidget) { /// [见2.7小节]
    _widget = newWidget;
  }

通过查看 [2.1] 类图,发现最下层的子类是 InheritedElement

2.7 InheritedElement#update

class InheritedElement extends ProxyElement {
	@override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget); /// [见2.8小节]
  }
}

widget.updateShouldNotify(oldWidget) 这个方法是不是似曾相识,这个就是一开始,我们说的 InheritedWidget.updateShouldNotify [见1.2.4小节],这里也验证之前说法,如果 true 表示会继续后续的,false 不执行后续的操作。我们接着看 super.updated 后面的流程。

2.8 ProxyElement#update

通过查看 [2.1] 类图, InheritedElement 继承自 ProxyElement

abstract class ProxyElement extends ComponentElement {
  @override
  ProxyWidget get widget => super.widget as ProxyWidget;

  @override
  Widget build() => widget.child;
  
  /// ...
  @override
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget;
		/// ...
    updated(oldWidget);
    _dirty = true;
    rebuild();
  }
}
  /// 当 widget 更改时, 在 build 期间调用。
  /// 默认情况下,调用 notifyClients。 子类可以覆盖此方法以避免不必要地调用 notifyClients(如果旧的和新的小部件是等效的)。
  @protected
  void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
  }
  /// 通知其他对象与此 Elment 关联的 Widget 已更改。
  /// 在更改与此元素关联的 Widget 之后但在重建此元素之前,在update期间(通过updated )调用
  @protected
  void notifyClients(covariant ProxyWidget oldWidget); // [见2.9小节]

2.9 InheritedElement#notifyClients

class InheritedElement extends ProxyElement {
  final Map<Element, Object> _dependents = HashMap<Element, Object>(); 
	
  /// [2.5.2小节] 初始化时调用
  @override
  void _updateInheritance() {
    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;
  }

  /// 返回使用 [setDependencies]为 [dependent] 记录的依赖项值。
  /// 每个依赖元素都映射到单个对象值,该值表示元素如何依赖此 [InheritedElement]。
  /// 默认情况下,此值为 null,并且默认情况下会无条件重建相关元素。
  /// 子类可以使用 [updateDependencies] 管理这些值
  /// 以便他们可以选择性地重建 [notifyDependent] 中的依赖项。
  /// 此方法通常仅在 [updateDependencies] 的覆盖中调用。
  /// 也可以看看:
  /// [updateDependencies],每次使用 [dependOnInheritedWidgetOfExactType] 创建依赖项时都会调用它。
  /// [setDependencies],设置依赖元素的依赖值。
  /// [notifyDependent],可以覆盖它以使用依赖项的依赖项值来决定是否需要重建依赖项。
  /// [InheritedModel],这是一个使用该方法管理依赖值的类的例子。
  @protected
  Object getDependencies(Element dependent) {
    return _dependents[dependent];
  }

  /// 为dependent设置getDependencies值返回的值。
  /// 每个依赖元素都映射到单个对象值,该值表示元素如何依赖此InheritedElement 。 
  /// [updateDependencies]方法在默认情况下将该值设置为null,以便无条件地重建依赖元素。
  /// 子类可以使用[updateDependencies]管理这些值,以便它们可以有选择地在[NotifyDependency]中重建依赖项。
  /// 此方法通常仅在updateDependencies覆盖中调用。
  @protected
  void setDependencies(Element dependent, Object value) {
    _dependents[dependent] = value;
  }

  /// 添加新的 [dependent] 时由 [dependOnInheritedWidgetOfExactType] 调用。
  /// 每个依赖元素都可以映射到单个对象值 [setDependencies]。 此方法可以使用 [getDependencies] 查找现有依赖项。
  /// 默认情况下,此方法将 [dependent] 的继承依赖项设置为 null。 这仅用于记录对 [dependent] 的无条件依赖。
  /// 子类可以管理自己的依赖项值,以便它们可以在 [notifyDependent] 中重建依赖项。
  /// [getDependencies],返回依赖项的当前值元素。
  /// [setDependencies],设置依赖元素的值。
  /// [notifyDependent],可以覆盖它以使用依赖的依赖值来决定是否需要重建依赖。
  /// [InheritedModel],这是一个使用该方法的类的例子管理依赖值。
  @protected
  void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }

  /// 由 [notifyClients] 为每个依赖调用。
  /// 默认情况下调用 [dependent.didChangeDependencies()] 。
	/// 子类可以覆盖此方法以根据 [getDependencies] 的值有选择地调用 [didChangeDependencies] 。
	/// 也可以看看:
	/// updateDependencies ,每次使用 [dependOnInheritedWidgetOfExactType] 创建依赖项时都会调用它。
	/// getDependencies ,它返回依赖元素的当前值。
	/// setDependencies ,它设置依赖元素的值。
	/// InheritedModel ,这是使用此方法管理依赖项值的类的示例。
  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies(); /// [见2.9.1]
  }

  /// 通过调用Element.didChangeDependencies通知所有依赖的Elements,这个widget已经更改。
  /// 此方法只能在 build 阶段调用。 通常当 Inherited widget 被重建时,这个方法会自动调用,例如,作为在 Inherited widget上方
  /// 调用State.setState的结果
  @override
  void notifyClients(InheritedWidget oldWidget) { 
    for (final Element dependent in _dependents.keys) {
      /// ...
      notifyDependent(oldWidget, dependent);
    }
  }
}
2.9.1 Element#didChangeDependencies
void didChangeDependencies() {
   markNeedsBuild();
}

看到 markNeedsBuild 是不是很熟悉。没错,就是我们经常使用 setState,调用一样的方法。

  void setState(VoidCallback fn) {
    // ...
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

上面我们发现 notifyClients,遍历循环 _dependents.keys,调用 Element.didChangeDependencies 更新依赖。我们看下 dependents 是怎么来的呢?我们看 updateDependencies 注释,是调用 context.dependOnInheritedWidgetOfExactType 添加 Element

2.10 Element#dependOnInheritedWidgetOfExactType

ElementBuildContext 具体实现类

  @override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
2.11 流程图

下图,只是简单画了 InheritedElement 更新依赖组件的大致流程图,方便大家吸收,具体的细节还需要自己去挖掘。

6.png

2.12 小结

dependOnInheritedWidgetOfExactType 通过 inheritedWidgets[T] 获取到对应的 InheritedElement,当 InheritedElement不为空,接着调用 InheritedElementupdateDependencies,把当前 Element 注入到要获取 InheritedElement_dependents.keys 集合,接着返回 InheritedElementwidget 对象。当InheritedWidget 更新时,先通过 updateShouldNotify 判断当前 InheritedElement 是否能进行 updated,当值为 true 时,通过循环遍历 _dependents.keys 集合,来更新所有依赖的 widget

三、自定义 Provider

前面写了一个简单的 计数器,同时也介绍了 InheritedWidget 更新依赖 widget 的原理分析。那我们能不能基于 InheritedWidget 实现一个自己的 Provider 功能呢?接下来,我们基于之前的 计数器 的代码,一步一步优化,实现一个我们自己的 Provider

3.1 优化方案(一)

通过上面我们能发现,在主页面直接调用 setState,是比较消耗性能,应该把 incrementCounter 操作剥离出去,单独一个 model,这样似乎更符合实际的开发。

3.1.1 CountModel

新建 CountModel 用来进行数据处理及页面更新操作,有点类似 androidviewModel

class CountModel with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void incrementCounter() {
    _count++;
    notifyListeners();
  }
}
3.1.2 CountProvider

初始化传入数据源 T, 继承 ChangeNotifier ,监听 T notifyListeners 时,刷新 CountProvider,顺便把 dependOnInheritedWidgetOfExactTypeCountScope 移到 CountProvider 类中。

typedef WidgetBuilder = Widget Function(BuildContext context);

class CountProvider<T extends ChangeNotifier> extends StatefulWidget {
  const CountProvider({
    Key key,
    this.value,
    this.builder,
  }) : super(key: key);

  final T value;

  final WidgetBuilder builder;

  static T of<T extends ChangeNotifier>(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CountScope<T>>().value;
  }

  @override
  _CountProviderState<T> createState() => _CountProviderState<T>();
}

class _CountProviderState<T extends ChangeNotifier> extends State<CountProvider<T>> {
  void _changeValue() {
    setState(() {});
  }

  @override
  void initState() {
    super.initState();
    widget.value?.addListener(_changeValue);
  }

  @override
  void dispose() {
    widget.value?.removeListener(_changeValue);
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant CountProvider<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.value != widget.value) {
      oldWidget.value?.removeListener(_changeValue);
    }
    widget.value?.addListener(_changeValue);
  }

  @override
  Widget build(BuildContext context) {
    print('_CountProviderState build');
    return CountScope<T>(
      value: widget.value,
      child: Builder(
        builder: (BuildContext context) {
          return widget.builder(context);
        },
      ),
    );
  }
}
3.1.3 CountScope
class CountScope<T extends ChangeNotifier> extends InheritedWidget {
  const CountScope({@required this.value, Widget child}) : super(child: child);

  final T value;

  @override
  bool updateShouldNotify(CountScope<T> oldWidget) {
    return true;
  }
}
3.1.4 CountWidget
class CountWidget extends StatefulWidget {
  const CountWidget({Key key}) : super(key: key);

  @override
  _CountWidgetState createState() => _CountWidgetState();
}

class _CountWidgetState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print('_CountWidgetState build');
    return Container(
      child: Text(
        '${CountProvider.of<CountModel>(context).count}',
        style: Theme.of(context).textTheme.headline4,
      ),
    );
  }
}
3.1.5 MyHomePage
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountProvider<CountModel>(
              value: CountModel(),
              builder: (BuildContext context) {
                return Column(children: [
                  const CountWidget(),
                  ClickButton(),
                ]);
              },
            ),
          ],
        ),
      ),
    );
  }
}
3.1.6 ClickButton
class ClickButton extends StatelessWidget {
  const ClickButton({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('ClickButton build');
    return ElevatedButton(
      onPressed: () {
        print('--- 点击 ---');
        CountProvider.of<CountModel>(context).incrementCounter();
      },
      child: const Icon(Icons.add),
    );
  }
}

当我们点击 + 时,能正常更新数据,我们看 log 会发现, MyHomePage 页面没有重新进行 build,如果我们在MyHomePageColumn 组件里面,再添加一个不依赖 CountProvider 的组件,会发生什么?

3.1.7 Nothing
class Nothing extends StatefulWidget {
  const Nothing({Key key}) : super(key: key);

  @override
  _NothingState createState() => _NothingState();
}

class _NothingState extends State<Nothing> {
  @override
  Widget build(BuildContext context) {
    print('_NothingState build');
    return Container(
      child: const Text('我是一个不依赖的 widget'),
    );
  }
}
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountProvider<CountModel>(
              value: CountModel(),
              builder: (BuildContext context) {
                return Column(children: [
                  const CountWidget(),
                  Nothing(), /// 新增
                  ClickButton(),
                ]);
              },
            ),
          ],
        ),
      ),
    );
  }
}

结果如下图:

可以发现,每次我们点击 + ,调用 CountModel 进行 notifyListeners 更新时,都会导致 CountProvider 的子 Widget 全部刷新,不管是否依赖。

3.1.8 小结

现在我们会发现有三个问题:

  1. 当前 Widget 调用 CountModel 方法时,可以不进行依赖绑定?
  2. 如果我们点击 Flutter Hot Reload 按钮,会发现计数器的 1,会变成 0,这合理吗?
  3. 有没有只会更新依赖的 widget,不更新 InheritedElement 组件下,所有的子 widget ?

3.2 优化方案(二)

3.2.1 InheritedElement

因为我们是在 CountProvider 中监听 CountScope 的数据源变化,调用 setState 刷新整个 CountProvider,导致子 widget 也都全部更新,所以,我们应该把数据源更新监听移到 CountScope,让 CountScope 通知刷新依赖的 widget 比较合理。

class CountScope<T extends ChangeNotifier> extends InheritedWidget {
  const CountScope({@required this.value, Widget child}) : super(child: child);

  final T value;

  @override
  bool updateShouldNotify(CountScope<T> oldWidget) {
    /// 因为是 InheritedElement 进行刷新,所以,这里可以设置为 false
    return false;
  }

  @override
  CountScopeElement<T> createElement() {
    return CountScopeElement<T>(value, this);
  }
}

class CountScopeElement<T extends ChangeNotifier> extends InheritedElement {
  CountScopeElement(this.value, InheritedWidget widget) : super(widget) {
    value?.addListener(_handleUpdate);
  }

  final T value;

  void _handleUpdate() {
    markNeedsBuild();
  }

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

  @override
  Widget build() {
    notifyClients(widget);
    return super.build();
  }
}

新建 CountScopeElement 继承 InheritedElement,在 CountScopecreateElement 返回自定义 CountScopeElement 类。在 CountScopeElement 实现数据源变化监听,在销毁时调用移除监听。

  1. _handleUpdate 调用 markNeedsBuild ,这个方法我们应该都很熟悉,也就是 setState 调用后,调用同样的方法,把 CountScopeElement 标记为 dirtybuild 调用 notifyClients(widget) ,可能会人有不明白,为什么这里要调用 notifyClients

  2. 通过 [2.8小节] 我们可以发现,当 CountScopeElement 进行 build 时,其实这里的 child 没有更新,还是同一个对象。[见2.5小节],当 child.widget == newWidget 时,是不进行 child.update() 操作,也就没有后续的一串依赖更新操作。所以,这里要重写 build ,手动调用内部的 notifyClients,通知依赖更新操作。

3.2.2 getElementForInheritedWidgetOfExactType

通过 [1.1小节] 我们知道,获取 InheritedWidgetdependOnInheritedWidgetOfExactType,并创建依赖关系。我们通过 getElementForInheritedWidgetOfExactType 获取 InheritedElement,这个过程,不创建依赖关系,似乎能解决需要依赖的问题,我们修改 CountProviderof 如下:

  static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
    final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
    final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }
    return countScopeElement.value;
  }

实现思路,获取到 CountScopeElement ,如果 listen = true 时,则进行依赖更新,最后返回 CountScopeElement.value

3.2.3 CountProvider
class CountProvider<T extends ChangeNotifier> extends StatefulWidget {
  const CountProvider({
    Key key,
    this.value,
    this.builder,
  }) : super(key: key);

  /// 数据源
  final T value;

  final WidgetBuilder builder;

  static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
    final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
    final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }
    return countScopeElement.value;
  }

  @override
  _CountProviderState<T> createState() => _CountProviderState<T>();
}

class _CountProviderState<T extends ChangeNotifier> extends State<CountProvider<T>> {

  @override
  Widget build(BuildContext context) {
    print('_CountProviderState build');
    return CountScope<T>(
      value: widget.value,
      child: Builder(
        builder: (BuildContext context) {
          return widget.builder(context);
        },
      ),
    );
  }
}

最终效果是可行的,也解决了 [3.1.8小节] 提出的三个问题。上面的代码看起来似乎没什么问题,如果 _MyHomePageState 在进行 setState 操作,会发现什么事情?

class _MyHomePageState extends State<MyHomePage> {
  void _refresh() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CountProvider<CountModel>(
              value: CountModel(),
              builder: (BuildContext context) {
                return Column(children: [
                  const CountWidget(),
                  Nothing(),
                  ElevatedButton(
                    onPressed: () {
                      print('--- 点击 ---');
                      CountProvider.of<CountModel>(context, listen: false).incrementCounter();
                    },
                    child: const Icon(Icons.add),
                  ),
                ]);
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _refresh,
        tooltip: 'refresh',
        child: const Icon(Icons.refresh),
      ),
    );
  }
}

每次点击 refresh,也会刷新 CountProvider 的依赖的子 widget ,这似乎并没有达到我们的要求。那该怎么解决呢?

3.3 优化(三)

3.3.1 InheritedBuildContext

抽象类, CountScopeElement 剥离出来需要用到功能和 value,方便后期扩展。

abstract class InheritedBuildContext<T> {
  void needsBuild();

  T get value;
}
3.3.2 Delegate

代理类 ,CountScopeElement 中方法的实现,value 的赋值。

abstract class Delegate<T> {
  CountScopeElement<T> element;

  T get value;
}
3.3.3 CountDelegate

Delegate 的实现类,主要实现了数据的监听,通知 InheritedElement 刷新依赖。

class CountDelegate<T extends ChangeNotifier> extends Delegate<T> {
  CountDelegate({this.notifier});

  T notifier;

  @override
  T get value {
    notifier.addListener(() {
      element.needsBuild();
    });
    return notifier;
  }
}
3.3.4 CountScope
class CountScope<T> extends InheritedWidget {
  const CountScope({@required this.delegate, Widget child}) : super(child: child);

  final Delegate<T> delegate;

  @override
  bool updateShouldNotify(CountScope<T> oldWidget) {
    return false;
  }

  @override
  CountScopeElement<T> createElement() {
    return CountScopeElement<T>(delegate, this);
  }
}

class CountScopeElement<T> extends InheritedElement with InheritedBuildContext<T> {
  CountScopeElement(this.delegate, InheritedWidget widget) : super(widget);

  final Delegate<T> delegate;
  bool _dirty = false;

  @override
  CountScope<T> get widget => super.widget as CountScope<T>;

  @override
  void performRebuild() {
    delegate.element = this;
    super.performRebuild();
  }

  @override
  Widget build() {
    if (_dirty) {
      _dirty = false;
      notifyClients(widget);
    }
    return super.build();
  }

  @override
  void needsBuild() {
    _dirty = true;
    markNeedsBuild();
  }

  @override
  T get value => delegate.value;
}

主要修改,当需要重新 build 时,_dirty 标记为 true,当 build 完成时,再设置为 false,避免当CountScope 刷新,导致依赖的子 widget 也全部刷新。

3.3.5 CountProvider

集成 SingleChildStatelessWidget

class CountProvider<T extends ChangeNotifier> extends SingleChildStatelessWidget {
  CountProvider({Key key, this.value, this.builder})
      : _delegate = CountDelegate<T>(
          notifier: value,
        ),
        super(key: key);

  final T value;
  final WidgetBuilder builder;
  final Delegate<T> _delegate;

  static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
    final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
    final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }
    return countScopeElement.value;
  }

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    print('CountProvider buildWithChild');
    return CountScope<T>(delegate: _delegate, child: Builder(builder: builder));
  }
}

最终结果:当 CountScope 重新 rebuild 时,依赖的子 widget 不会跟着一起 rebuild

3.3.5 小结

小结:这里优化三个方案,也只是带大家发现问题,一步一步去解决问题。其实,跟大家的业务结合的话,这上面的代码,或许还会有不满足务求的地方。但是,不管什么框架,也都是一步一步优化过来的。只要有了解决问题的思路和想法,我相信,任何问题都不在是问题。

四、 总结

前面分析 InheritedWidget 的源码,调用流程等,在实际开发过程中,我们可能不会直接接触 InheritedWidget ,但是,还是能发现一些系统的组件,使用 InheritedWidget 进行封装开发。例如:ThemeFocusButtonBarTheme 等,就不一一举例了,大家可以去看下源码。写这篇文章,一方面是为了巩固知识,方便日后查看、回忆知识点。另一方便也是抛砖引玉,让更多人了解 InheritedWidget,如果大家认真看,其实还是会发现一些 provider 的影子。

如果文章有什么不足的地方,欢迎批评指正,谢谢~