Widget基础系列 - InheritedWidget

在Flutter中,Widget可以说是第一基础概念。Widget是对用户界面的不可变描述,可被膨化为管理底层渲染树的Element。

理解Widget原理是掌握Flutter编程至关重要的一步,本系列主要介绍Widget的基础知识,本文是第三篇:

  • StatelessWidget
  • StatefulWidget
  • InheritedWidget
  • Key

简介

前面讲过了StatelessWidget和StatefulWidget,当我们的App变得越来越大、Widget树越来越复杂时,传递和访问数据会变得非常繁琐。

widget_tree

假设每个Widget嵌套了四、五个Widget,我们希望在最底层的Widget访问顶层Widget的数据,如果只使用StatelessWidget和StatefulWidget,我们不得不将数据添加在每个Widget的构造函数里,如果需要添加、修改、删除数据,那简直就是噩梦,一串构造函数都需要修改。你应该已经猜到了,InheritedWidget就是解决这个问题的。

widget_tree_inherited

当我们将InheritedWidget添加到Widget树中时,在其下面的所有Widget都可以得到一个指向它的引用,因此我们称其为Inherited Widget。

假设有一个InheritedWidget的子类:InheritedAppState:

class InheritedAppState extends InheritedWidget {
  final AppState appState;
  
  InheritedAppState({this.appState, Widget child}): super(child: child);
  
  @override
  bool updateShouldNotify(_InheritedStateContainer old) => true;
  
  static InheritedAppState of(BuildContext context) =>
    context.inheritFromWidgetOfExactType(InheritedAppState);
}

在子孙Widget中,可以直接通过context.inheritFromWidgetOfExactType(InheritedAppState)获取InheritedAppState对象。为了提升代码简洁性和易读性,InheritedWidget一般会包含一个静态的of方法。

我们注意到,InheritedWidget是不可变的,因此上面的appState属性是final类型的。当InheritedWidget的数据改变时,我们只能重建整个widget。请谨记这一点,所有的Widget都是不可变的,不要改变widget中保存的数据。

final仅表示属性不能被再次赋值,不代表内部不能改变。比如,我们可以将附加的数据模型改为一个service对象,这个service可以访问数据库、网络请求、本地assets资源等。

class InheritedAppState extends InheritedWidget {
  final AppStateService service;
  
  InheritedAppState({this.service, Widget child}): super(child: child);
  
  @override
  bool updateShouldNotify(_InheritedStateContainer old) => true;
  
  static InheritedAppState of(BuildContext context) =>
    context.inheritFromWidgetOfExactType(InheritedAppState);
}

简单来说,InheritedWidget提供了一种在widget树中从上到下传递、共享数据的方式。Flutter SDK正是通过InheritedWidget来共享应用主题和Locale等信息的。

原理

对于InheritedWidget来说,需要实现:

  • 在子孙widget中可以方便地获取到InheritedWidget或者保存在InheritedWidget中的数据
  • InheritedWidget中的数据改变时,更新所有依赖的子孙widget

下面我们分别介绍实现原理。

获取InheritedWidget对象

我们已经知道,通过context.inheritFromWidgetOfExactType可以在子孙widget中获取上层的InheritedWidget对象,具体是如何实现的呢?

BuildContext实际上就是Element,其相关实现代码如下:

Map<Type, InheritedElement> _inheritedWidgets;

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

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

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

可以看到,在Element中,有一个Map类型的属性_inheritedWidgets,键是Type,而值是InheritedElement。当我们调用inheritFromWidgetOfExactType方法时,直接从这个Map中查找对应类型的InheritedElement,并调用其updateDependencies方法注册依赖,然后返回其关联的widget对象。

那么_inheritedWidgets的值从哪儿来呢?从parent那儿来。那parent的值又从哪儿来呢?从距离最近的InheritedElement祖先那儿来。InheritedElement中的_inheritedWidgets又是如何维护的呢?

@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中的值,并注册自身。

因此,一个普通Element中的_inheritedWidgets,保存的就是其所有InheritedElement祖先节点的信息,通过这个属性,我们就可以方便地、高效率地获取到树上层的InheritedWidget了。

更新子孙widget

这个相对简单,不涉及新的知识,当上层的InheritedWidget变化时,其widget子树会重建,子孙widget在其build方法中就可以获取到新的值了。

为了确保子孙widget中的数据能及时更新,只应该在build方法、layout和paint回调以及State.didChangeDependencies方法中调用inheritFromWidgetOfExactType方法。

不应该在widget的构造方法或者State.initState方法中调用inheritFromWidgetOfExactType方法,因为当数据变更时,这些方法不会被再次调用。也不应该在State.dispose方法中调用,因为此时element树是不稳定的。如果必须要在State.dispose中访问上层的InheritedWidget,可以提前在State.didChangeDependencies方法中保存一个引用。在State.deactivate方法中调用是安全的,此方法在widget从树中移除时被调用 。

如果我们希望在InheritedWidget更新时做一些处理怎么办?比如从网络获取数据,如果放在build中代价显然太高了,从设计模式的角度来说也极其不合理,build方法最好只负责构建widget树。

State有一个didChangeDependencies方法,当其依赖的对象发生改变时会被调用,最主要的场景就是InheritedWidget依赖。此方法在initState方法结束之后也会被马上调用一次,可以在这个方法中安全地调用BuildContext.inheritFromWidgetOfExactType方法。子类很少需要重写此方法,因为框架总会在依赖改变时调用build方法。只有当需要执行网络请求这样的昂贵操作时,才需要重写此方法。

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

推荐阅读更多精彩内容