Flutter InheritedWidget源码解析

介绍

InheritedWidget是flutter中用来从上层传递数据到下层的组件(类似于React中的Context)。对于简单的情况,如果需要将数据传递到下层,可以通过参数的方式。如下所示

class ParentWidget extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
        var value = "this is the value";
        return ChildWidget(value: value);
    }
}

class ChildWidget extends StatelessWidget {

    String value;

    ChildWidget({Key key, this.value}): super(key);

    @override
    Widget build(BuildContext context) {
        return Text(value);
    }
}

这种方式在Widget层级不是很深的时候比较方便,但是如果Widget层级很多,就需要从上层一层一层的向下传递,容易造成混乱。所以flutter提供了InheritedWidget方便上层Widget跨层级为下层子节点提供数据。

使用方式

可以创建一个Widget继承自InheritedWidget, 在Widget中保存需要共享给子Widget的值。创建一个静态方法(名称通常为of),该方法接收BuildContext参数,通过调用BuildContextdependOnInheritedWidgetOfExactType可以从下层找到该Widget,从而获取Widget中的值。

class FrogColor extends InheritedWidget {
   const FrogColor({
            Key key,
     @required this.color,
     @required Widget child,
   }) : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

   final Color color;

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

   @override
   bool updateShouldNotify(FrogColor old) => color != old.color;
 }

在定义InheritedWidget的时候,还需要重写updateShouldNotify方法。该方法作用是,当InheritedWidget重新build的时候,是否要通知依赖于该Widget的子Widget也要重新build。一般是在这个方法中比较InheritedWidget所持有的值是否变化,如果变化了,就通知子Widget重新build,以更新状态。

下层获取可以通过of方法获取值

    Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: FrogColor(
          color: Colors.green,
          child: Center(
            child: ChildWidget(),
          )
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

  class ChildWidget extends StatelessWidget {

      Widget build(BuildContext context) {
        var color = FrogColor.of<FrogColor>(context).color;
        return Text(
            "color from parent",
            style: TextStyle(
                color:color
            ));
      }
  }

可以看到,在子Widget中并没有设置color参数,而是通过FrogColor.of(context).color获取到了祖先Widget的值,即使中间跨越了其他Widget。那么是通过什么来访问到祖先Widget的数据呢,关键点就是dependOnInheritedWidgetOfExactType方法。

源码分析

该方法在BuildContext中,而BuildContext的实现类是Element,我们来看下这个方法的源码。(还有一个inheritFromWidgetOfExactType方法,这个方法的实现和dependOnInheritedWidgetOfExactType一样,在1.12版本中被deprecated了)

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

方法非常简单,首先使用泛型T作为参数(Dart中的泛型比Java强大一些,没有被擦除),从_inheritedWidgets中获取类型为InheritedElementancestor对象,该对象就是InheritedWidget所对应的Element对象。如果这个ancestor对象不为空,再调用dependOnInheritedElement方法。

Map<Type, InheritedElement> _inheritedWidgets;

_inheritedWidgets是在Element类中定义的一个Map, 保存了所有的所有的祖先InheritedWidget。这个_inheritedWidgets是从哪里初始话的呢?

Element中的_inheritedWidgets初始化流程

通过搜索可以找到在_updateInheritance方法中会更新_inheritedWidgets Map,_updateInheritance是也是在Element类中的定义,默认是实现是将parent的值赋给自己。

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

在InheritedElement中重写了_updateInheritance方法,不会直接赋值,而是通过HashMap.from方法重新创建了一个Map,并且通过_inheritedWidgets[widget.runtimeType] = this; 将InheritedElement自己保存到这个Map中。

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

通过上面的方式,就可以一层一层的传递保存InheritedEelement的Map,非InheritedWidget直接保存parent的_inheritedWidgets的引用,是同一个Map对象。而InheritedWidget对应的InheritedElement会创建一个新的Map,并将对应widget的runtimeType作为key,element作为Value保存在Map中。所以底层的Element的_inheritedWidgets Map中包含所有类型为InheritedWidget的祖先节点信息。就可以通过Widget 的Type访问到Element,再通过Element访问到Widget。

_updateInheritance方法在Element被挂载到Element Tree时(mount)或者activate的时候被调用。

void mount(Element parent, dynamic newSlot) {
    ...
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null) // Only assign ownership if the parent is non-null
      _owner = parent.owner;
    final Key key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    _updateInheritance();
    ...
  }

   void activate() {
    ...
    final bool hadDependencies = (_dependencies != null && _dependencies.isNotEmpty) || _hadUnsatisfiedDependencies;
    _active = true;
    _dependencies?.clear();
    _hadUnsatisfiedDependencies = false;
    _updateInheritance();
    ...
    if (_dirty)
      owner.scheduleBuildFor(this);
    if (hadDependencies)
      didChangeDependencies();
  }

dependOnInheritedElement

分析完_inheritedWidgets的初始化流程,我们回头再看dependOnInheritedElement方法。

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

dependOnInheritedElement中首先会将ancestor保存在_dependencies中,再调用ancestor
updateDependencies方法。

  void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }

updateDependencies方法再调用setDependencies方法

  final Map<Element, Object> _dependents = HashMap<Element, Object>();

 void setDependencies(Element dependent, Object value) {
    _dependents[dependent] = value;
  }

setDependencies方法底层的Element保存在_dependents Map中。

以上就是通过BuildContext的dependOnInheritedWidgetOfExactType访问到祖先InhritedWidget的过程。

updateShouldNotify方法

在定义InheritedWidget时还需要重写updateShouldNotify方法,这个方法会在InheritedEelement中的updated方法中被调用

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

调用updateShouldNotify方法,如果返回true就再调用super.updated(oldWidget)方法,否则,不执行任何操作。
InheritedElement的父类是ProxyElementProxyElementupdated方法会调用notifyClinets(oldWidget)方法

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

notifyClinets(oldWidget)方法是ProxyElement定义的抽象方法,在InheritedElement类中有具体的实现如下:

 @override
  void notifyClients(InheritedWidget oldWidget) {
    ...
    for (final Element dependent in _dependents.keys) {
      ...
      notifyDependent(oldWidget, dependent);
    }
  }

会循环遍历_dependents列表,调用notifyDependent(oldWidget, dependent)方法

 @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }

notifyDependent方法会直接调用dependentdidChangeDependencies方法,这个方法定义在Element类中,实现如下

void didChangeDependencies() {
    ...
    markNeedsBuild();
  }

didChangeDependencies方法会调用Element的markNeedsBuild方法,而markNeedsBuild方法会将该element标记为dirty,在下一帧将会重新build。

总结

将特定的值保存在InheritedWidget中,Elemment树中的每一个节点都会保存祖先节点的InheritedWidget信息,所以可以在子组件中可以查询到特定类型的InheritedWidget,并获取到其所保存的值。当InheritedWidget被重新build时,会根据updateShouldNotify方法决定是否通知依赖该InheritedWidget的子Widget更新。

在flutter framework源码中大量使用了InheritedWidget实现数据的传递和共享,通过源码分析可以对其原理进行深入的理解但在我们日常开发中,还是推荐使用Provider,Provider提供了更简单的API和更强大的功能。

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