Flutter源码分析系列(二):Widget数据共享之InheritedWidget

简介

业务开发中经常会碰到这样的情况,多个Widget需要同步同一份全局数据,比如点赞数、评论数、夜间模式等等。在安卓中,一般的实现方式是观察者模式,需要开发者自行实现并维护观察者的列表。在flutter中,原生提供了用于Widget间共享数据的InheritedWidget,当InheritedWidget发生变化时,它的子树中所有依赖了它的数据的Widget都会进行rebuild,这使得开发者省去了维护数据同步逻辑的麻烦。

使用范例

先来看下官方注释中的范例:

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.inheritFromWidgetOfExactType(FrogColor);
  }
  
  @override
  bool updateShouldNotify(FrogColor old) => color != old.color;
}

实现比较简单,直接继承InheritedWidget即可,类中的color即为需要共享的数据。
此外需要实现updateShouldNotify方法,当InheritedWidget被更新时,该方法会被调用,并传入更新之前的Widget对象,该方法内需要实现新旧数据的比较,若数据不同需要通知依赖的Widget更新,则返回true。
使用时,可以通过FrogColor.of(context).color获取到它的值。
下面举一个具体的例子,简单的使用场景如下图所示:

InheritedWidget范例示意图

将最上层的Widget用InheritedWidget包装起来,并设置颜色为绿色,子Widget中需要使用该颜色时调用FrogColor.of(context).color来获取,如图中的Icon和Image,此时调用该方法获取到的颜色即为绿色。当InheritedWidget的数据有变化,即通过setState将绿色改为红色后,依赖了该数据的Icon和Image会自动rebuild,构造新的Widget时,此时从FrogColor.of(context).color获取到的颜色也会变为红色,由此便实现了数据的同步。

源码分析

InheritedWidget

先来看下InheritedWidget的源码:

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

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

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

它继承自ProxyWidget:

abstract class ProxyWidget extends Widget {
  const ProxyWidget({ Key key, @required this.child }) : super(key: key);

  final Widget child;
}

可以看出Widget内除了实现了createElement方法外没有其他操作了,它的实现关键一定就是InheritedElement了。

InheritedElement

class InheritedElement extends ProxyElement {
  InheritedElement(InheritedWidget widget) : super(widget);

  @override
  InheritedWidget get widget => super.widget;

  // 这个Set记录了所有依赖的Element
  final Set<Element> _dependents = new HashSet<Element>();

  // 该方法会在Element mount和activate方法中调用,_inheritedWidgets为基类Element中的成员,用于提高Widget查找父节点中的InheritedWidget的效率,它使用HashMap缓存了该节点的父节点中所有相关的InheritedElement,因此查找的时间复杂度为o(1)
  @override
  void _updateInheritance() {
    final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = new HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = new HashMap<Type, InheritedElement>();
    _inheritedWidgets[widget.runtimeType] = this;
  }

  // 该方法在父类ProxyElement中调用,看名字就知道是通知依赖方该进行更新了,这里首先会调用重写的updateShouldNotify方法是否需要进行更新,然后遍历_dependents列表并调用didChangeDependencies方法,该方法内会调用mardNeedsBuild,于是在下一帧绘制流程中,对应的Widget就会进行rebuild,界面也就进行了更新
  @override
  void notifyClients(InheritedWidget oldWidget) {
    if (!widget.updateShouldNotify(oldWidget))
      return;
    for (Element dependent in _dependents) {
      dependent.didChangeDependencies();
    }
  }
}

其中_updateInheritance方法在基类Element中的实现如下:

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

总结来说就是Element在mount的过程中,如果不是InheritedElement,就简单的将缓存指向父节点的缓存,如果是InheritedElement,就创建一个缓存的副本,然后将自身添加到该副本中,这样做会有两个值得注意的点:

  1. InheritedElement的父节点们是无法查找到自己的,即InheritedWidget的数据只能由父节点向子节点传递,反之不能。
  2. 如果某节点的父节点有不止一个同一类型的InheritedWidget,调用inheritFromWidgetOfExactType获取到的是离自身最近的该类型的InheritedWidget。

看到这里似乎还有一个问题没有解决,依赖它的Widget是在何时被添加到_dependents这个列表中的呢?
回忆一下从InheritedWidget中取数据的过程,对于InheritedWidget有一个通用的约定就是添加static的of方法,该方法中通过inheritFromWidgetOfExactType找到parent中对应类型的的InheritedWidget的实例并返回,与此同时,也将自己注册到了依赖列表中,该方法的实现位于Element类,实现如下:

@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
  // 这里通过上述mount过程中建立的HashMap缓存找到对应类型的InheritedElement
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
  if (ancestor != null) {
    // 这个列表记录了当前Element依赖的所有InheritedElement,用于在当前Element deactivate时,将自己从InheritedElement的_dependents列表中移除,避免不必要的更新操作
    _dependencies ??= new HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    // 这里将自己添加到了_dependents列表中,相当于注册了监听
    ancestor._dependents.add(this);
    return ancestor.widget;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

到此InheritedWidget就分析完了,相比观察者模式,使用它是不是方便了很多呢?

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

推荐阅读更多精彩内容

  • 本文介绍了Flutter应用程序中Widget,State,BuildContext和InheritedWidge...
    __white阅读 2,876评论 1 6
  • 哪有什么完美主义者,完美主义者就是失败者的另一种饰词。
    Chrisjiang阅读 322评论 0 0
  • 读假期安全提示后,我知道了很多的安全常识,如:父母外出不在家时的居家安全常识、假期出去时游玩的活动安全常识、旅游外...
    _何欣_阅读 426评论 0 3
  • 令水成冰 冻住包裹的过往 过往如水 匆忙奔去他乡 他乡何方? 何方? 人云亦云,匆匆过往 冰封凝结时 回首 消散如...
    糊涂兔adlen阅读 174评论 0 0