这两天学习了一下 Flutter 中的 InheritedWidget 的使用方法,顺便查看一下相关源码了解了其底层实现机制。特地记录一下。
Prerequirements
由于本文主要是从源码的角度分析 InheritedWidget 的工作原理,所以对阅读本文的小伙伴的 Flutter 知识有一定的要求。主要有以下几点,如果其中某部分你还不太清楚,请先阅读相关链接:
- 了解 Flutter 的基本用法。
- 了解 Flutter 中的 Widget 和 Element 的基本概念。 推荐阅读:Flutter, what are Widgets, RenderObjects and Elements?
- 对 Flutter 中 Element 的生命周期有基本了解。推荐阅读:Element class
下面开始正文。
InheritedWidget 的使用方法
先看一个 InheritedWidget 最简单的使用示例:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyWelcomeInfo extends InheritedWidget {
MyWelcomeInfo({Key key, this.welcomeInfo, Widget child})
: super(key: key, child: child);
final String welcomeInfo;
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return oldWidget.welcomeInfo != welcomeInfo;
}
}
class MyNestedChild extends StatelessWidget {
@override
build(BuildContext context) {
final MyWelcomeInfo widget =
context.inheritFromWidgetOfExactType(MyWelcomeInfo);
return Text(widget.welcomeInfo);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter InheritWidget',
home: MyWelcomeInfo(
welcomeInfo: 'hello flutter',
child: Center(
child: MyNestedChild(),
)),
);
}
}
可以看出我们使用 InheritedWidget 时涉及到的工作量主要有 2 部分:
- 创建一个继承自 InheritedWidget 的类,并将其插入 Widget 树
- 通过 BuildContext 对象提供的
inheritFromWidgetOfExactType
方法查找 Widget 树中最近的一个特定类型的 InheritedWidget 类的实例
这里还暗含了一个逻辑,那就是当我们通过 inheritFromWidgetOfExactType
查找特定类型 InheritedWidget 的时候,这个 InheritedWidget 的信息是由父元素层层向子元素传递下来的呢?还是 inheritFromWidgetOfExactType
方法自己层层向上查找的呢?
接下来让我们从源码的角度分别看看 Flutter 框架对以上几部分的实现。
实现原理分析
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
的抽象类。内部没什么逻辑,除了实现了一个 createElement
方法之外,还定义了一个 updateShouldNotify()
接口。 每次当 InheritedElement 的实例更新时会执行该方法并传入更新之前对应的 Widget 对象,如果该方法返回 true
那么依赖该 Widget 的(在 build 阶段通过 inheritFromWidgetOfExactType
方法查找过该 Widget 的子 widget)实例会被通知进行更新;如果返回 false
则不会通知依赖项更新。这个机制和 React 框架中的 shouldComponentUpdate
机制很像。
InheritedWidget 相关信息的传递机制
每个 Element 实例上都有一个 _inheritedWidgets
属性。该属性的类型为:
HashMap<Type, InheritedElement>
其中保存了祖先节点中出现的 InheritedWidget 与其对应 element 的映射关系。在 element 的 mount
阶段和 active
阶段,会执行 _updateInheritance()
方法更新这个映射关系。
对于普通 Element 实例,_updateInheritance()
只是单纯把父 element 的 _inheritedWidgets
属性保存在自身 _inheritedWidgets
里。从而实现映射关系的层层向下传递。
void _updateInheritance() {
assert(_active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
由 InheritedWidget 创建的 InheritedElement 重写了该方法:
void _updateInheritance() {
assert(_active);
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;
}
可以看出 InheritedElement 实例会把自身的信息添加到 _inheritedWidgets
属性中,这样其子孙 element 就可以通过前面提到的 _inheritedWidgets
的传递机制获取到此 InheritedElement 的引用。
InheritedWidget 的更新通知机制
首先让我们回答一个小问题,前文提到 _inheritedWidgets
属性存在于 Element 实例上,而我们代码中调用的 inheritFromWidgetOfExactType
方法则存在于 BuildContext 实例之上。那么 BuildContext 是如何获取 Element 实例上的信息的呢?答案是不需要获取。因为每一个 Element 实例也都是一个 BuildContext 实例。这一点可以从 Element 的定义中得到:
abstract class Element extends DiagnosticableTree implements BuildContext {
}
而每次 Element 实例执行 Widget 实例的 build
方法时传入的 context 就是该 Element 实例自身,以 StatelessElement 为例:
class StatelessElement extends ComponentElement {
...
@override
Widget build() => widget.build(this);
...
}
大家也可以在 IDE 中通过断点验证这一点。
既然可以拿到 InheritedWidget 的信息了,那接下让我们通过源码看看更新通知机制的具体实现。
首先看一下 inheritFromWidgetOfExactType
的实现:
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
_dependencies ??= new HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor._dependents.add(this);
return ancestor.widget;
}
_hadUnsatisfiedDependencies = true;
return null;
}
首先在 _inheritedWidget
映射中查找是否有特定类型 InheritedWidget 的实例。如果有则将该实例添加到自身的依赖列表中,同时将自身添加到对应的依赖项列表中。这样该 InheritedWidget 在更新后就可以通过其 _dependents
属性知道需要通知哪些依赖了它的 widget。
每当 InheritedElement 实例更新时,会执行实例上的 notifyClients
方法通知依赖了它的子 element 同步更新。notifyClients
实现如下:
void notifyClients(InheritedWidget oldWidget) {
if (!widget.updateShouldNotify(oldWidget))
return;
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (Element dependent in _dependents) {
assert(() {
// check that it really is our descendant
Element ancestor = dependent._parent;
while (ancestor != this && ancestor != null)
ancestor = ancestor._parent;
return ancestor == this;
}());
// check that it really depends on us
assert(dependent._dependencies.contains(this));
dependent.didChangeDependencies();
}
}
首先执行相应 InheritedWidget 上的 updateShouldNotify
方法判断是否需要通知,如果该方法返回 true
则遍历 _dependents
列表中的 element 并执行他们的 didChangeDependencies()
方法。这样 InheritedWidget 中的更新就通知到依赖它的子 widget 中了。
写在最后
本文简单的从源码的角度讨论了 InheritedWidget 的实现原理。由于 Flutter 框架本身也是使用 Dart 语言开发的,在熟悉了 Dart 语言后其实阅读 Flutter 源码并没有想象中的那么难,大家也可以根据自己感兴趣的点去有选择的阅读部分源码。
由于作者也是 Flutter 的初学者,文中可能存在描述不准确甚至是错误的地方。欢迎大家一起讨论。