flutter InheritedWidget 的奥秘

1,187 阅读5分钟

flutter中有个叫InheritedWidget 的widget,它是用来在组件树中共享数据的Widget,本身并不渲染任何视图,相当于一个代理Widget.有了它我们可以在组件树的最底层,访问到顶部你想要的数据。

本文不是介绍你怎么用它,而是介绍它工作的奥秘所在。

疑惑

  • 我是如何在它的子节点中拿到我共享的值的?
  • 共享的值变化后它怎么知道要让哪个子节点刷新UI呢?
  • 它的updateShouldNotify是怎么起作用的?

我是如何在它的子节点中拿到我共享的值得?

我们在写flutter 的Widget时flutter会对应给每个Widget生成一个element,比如StatefulWidget就会对应一个StatefulElement,我们用的 context 其实就是element,注意这个context跟安卓里面的context其实我认为有区别,安卓每个activity就是一个context,但是视图树中的View并不是一个context 而flutter因为没有页面容器这个概念,所以context是任何Widget对应的Element,如果不了解Widget和Element的关系,建议看这个大佬的文章

接着说在flutter构建视图的过程中,每个element都保存了一个属性叫_inheritedWidgets,来看下定义

 Map<Type, InheritedElement> _inheritedWidgets;

哟,是个Map,那肯定是来保存数据的啦,value是InheritedElement,这个InheritedElement是跟InheritedWidget 对应的,也就是说每个InheritedWidget 多会生成一个与它对应的InheritedElement, 这个_inheritedWidgets是Element基类的一个属性,那ok了,flutter各种类型的Element都是基类Element的子类,那每个Element就都有个这个属性咯,那在什么时候赋值的呢?

here:

  void _updateInheritance() {
    assert(_active);
    _inheritedWidgets = _parent?._inheritedWidgets;
  }

这个_updateInheritance 一共有两处调用到:

  • mount:这个是element被挂载到element树中时,一般是第一次渲染时调用
  • activate:这个是当你的element从inactive到active时调用

我们发现其实每个element都会在挂载时给_inheritedWidgets赋值,其实这个值 都会从parent来获取得一个引用而已,我们还没有看到真正给这个map在哪里赋值的,通过全局搜索 _inheritedWidgets = 我找到了在InheritedElement类里面的这个方法

  @override
  void _updateInheritance() {
    assert(_active);
    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;
  }

同样先从parent那里获取一下,如果parent没有就new一个新的 紧接着把widget.runtimeType当做key,把this当做value赋值到_inheritedWidgets,这个widget.runtimeType就是一个InheritedWidget 不过一般是子类,因为InheritedWidget 是个抽象类,那么你就知道为什么我们获取共享的值得时候必须要传InheritedWidget的子类名称过去了,它是根据子类的运行时名称当做key到这个map来找element的

目前我们知道它怎么赋值的了,那下一步怎么获取呢? 先去看一下这个文章 初学Flutter基础:关于InheritedWidget的理解的使用教程,介绍了我们怎么使用InheritedWidget

其中关键方法就是这个

  @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

使用InheritedWidget 有个约定俗成的方法就是我们写个静态of方法,在里面调用

context.inheritFromWidgetOfExactType(SomeWidget);

这个SomeWidget就是你继承InheritedWidget 的子类Widget的名字,代码很简单直接从_inheritedWidgets这个map来找value,如果ancestor 不等于null执行并返回这个方法


  @override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

我们暂且先看最后一行,返回了ancestor.widget,这个widget就是我们的InheritedWidget 的子类,我们一般会给子类的value传入共享的值,那得到widget那就得到共享的值了

总结一下:简单讲就是flutter保存记录了所有的InheritedWidget ,根据map的key直接找到value,O(1)的时间复杂度。

共享的值变化后它怎么知道要让哪个子节点刷新UI呢?

我们知道如果我们共享的值发生变化后我们是需要让用到这个值得节点刷新UI的,那它又是如果做到知道该让哪个刷新呢?

首先我们可以想想,要想知道通知哪个,那肯定要保存所有依赖这个value的对象,那什么时候收集呢?答案就是你用的时候我趁机收集一下,因为你不用我也不知道你需要更新啊

想想我们怎么用的,就是上面提到的

  @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

这里我们似乎没找到在哪里收集依赖的,再进去看看inheritFromElement 再来看一遍这个


  @override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

ok,看到了吗? ancestor.updateDependencies(this, aspect);这就是在收集依赖啊,这个方法内部就是把当前Element保存了一下,也就是传过去的this,那么此时InheritedWidget 已经知道了哪个子节点依赖了它的value,下一步我们看一下通知的过程,我们放在下一个问题一起说

它的updateShouldNotify是怎么起作用的?

我们接下来说,如果通知他的依赖的话,当我们的值变化后我们需要让InheritedWidget 进行一次rebuild, 在rebuild的过程中InheritedWidget会通知它的依赖rebuild 首先呢InheritedWidget 进行更新,这个更新跟常规的Element更新有点小区别 主要区别在这个地方

InheritedElement是个ProxyELement,它有个方法是

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

一看,notifyClients那肯定跟通知依赖有关 但是InheritedElement重写了这个方法

  @override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
  }

哇,原来在这里调用了updateShouldNotify,如果是true就调用父类方法,父类里面就去notifyClients(oldWidget);,如果是false就不通知依赖。注意这个也只有ProxyELement有这个方法,其他的StatefulElement就没有这个updated方法。

总结

看了不知道有没有让你有一点点了解它的工作原理,当然因为篇幅有限,我有些步骤会省略,如果你们感兴趣可以直接打断点调试,看程序一步步怎么走的,就对InheritedWidget的工作整个流程有所了解。

广告时间:我通过研究flutter的更新过程,开发了一个Widget,叫should_rebuild,可以自定义一个条件,来决定是否需要让Widget build,任何flutter Widget都可以哦,感兴趣的去看下哦should_rebuild,记得给star哦