在介绍Flutter provider这一篇的时候,我们提到了InhritedWidget,并介绍了Provider插件的底层就是通过封装InhritedWidget实现的。InhritedWidget提供了一种在 Widget 树中从上到下共享数据的方式。还有一些其他的框架,比如 Bloc、 Redux等都是使用了 InheritedWidget。
简介
InheritedWidget 组件是功能型组件,提供了沿树向下,共享数据的功能,即子组件可以获取父组件(InheritedWidget 的子类)的数据,通过BuildContext.dependOnInheritedWidgetOfExactType 获取。InheritedWidget 组件的共享数据是沿着树从上到下。
试想一下下面的这个场景,有一颗组件树,A、F 组件依赖同一数据,如下:A、F 组件要如何获取到数据呢?
有一种实现方式是 通过构造函数透传,数据通过A传递给B,B传递给C、E,C和E在传递给F,如下图虚线的传递:
return A(
data:data
child:B(
data:data
child:C(
data:data
child:F(
data:data
)
)
)
);
这样的实现缺点非常明显,B、C组件不需要 data 数据,如果组件树比较深的话,那将是噩梦。为了处理此问题,Flutter Framework 提供了 InheritedWidget 组件。
简单使用
InheritedWidget Widget 允许它的子 Widget 访问父 Widget 中的数据,使用它可以省去在 Widget 之间传递的数据的麻烦,任意的子 Widget 都可以共享这些数据。
- 首先编写一个继承自 InheritedWidget 的类,代码如下
class InheritedInfoWidget extends InheritedWidget {
InheritedInfoWidget({
required this.number,
required this.child,
Key? key,
}) : super(
key: key,
child: child,
);
final int number;
final Widget child;
@override
bool updateShouldNotify(covariant InheritedInfoWidget oldWidget) {
return oldWidget.number != number;
}
static InheritedInfoWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<InheritedInfoWidget>();
}
}
- 然后再编写一个 InfoChildWidget 来展示数字,代码如下:
class InfoChildWidget extends StatefulWidget {
const InfoChildWidget({Key? key}) : super(key: key);
@override
State<InfoChildWidget> createState() => _InfoChildWidgetState();
}
class _InfoChildWidgetState extends State<InfoChildWidget> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
//父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
//如果build中没有依赖InheritedWidget,则此回调不会被调用。
print('===== Dependencies change======');
}
@override
Widget build(BuildContext context) {
final int number = InheritedInfoWidget.of(context)!.number;
return Text('$number');
}
}
- 增加一个界面,用于包含以上组件
class InheritedDemoPage extends StatefulWidget {
const InheritedDemoPage({Key? key}) : super(key: key);
@override
State<InheritedDemoPage> createState() => _InheritedDemoPageState();
}
class _InheritedDemoPageState extends State<InheritedDemoPage> {
int _number = 0;
void _incrementCounter() {
setState(() {
_number++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('inherited page demo'),
centerTitle: true,
),
body: InheritedInfoWidget(
number: _number,
child: Center(
child: InfoChildWidget(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(
Icons.add,
),
),
);
}
}
从上面的代码我们可以看出,InheritedDemoPage State 中 的 _number 变量值在 InfoChildWidget 中通过 InheritedInfoWidget.of(context)!.number 可以直接使用,而无需通过后传递参数。
此时每当我当我们点击一下底部 ?,didChangeDependencies 便会调用,当 State 对象的依赖发生变化时会进行调用,而这个“依赖”指的就是子 widget 是否使用了父 widget 中 InheritedWidget 的数据。如果使用了,则代表子 Widget 有依赖;如果没有使用则代表没有依赖。这种机制可以使子组件在所依赖的 InheritedWidget 变化时来更新自身,例如:例如系统语言 Locale 或者应用主题等。
如果我们调整InfoChildWidget代码如下:
class InfoChildWidget extends StatelessWidget {
const InfoChildWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text('number');
}
}
我们会发现点击+的时候,InfoChildWidget的build不会被调用。
深入
首先看一下 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 机制很像。
每个 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 的引用。
数据更新
首先看一下 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,以及InheritedWidget能帮我们解决哪些问题,然后从源码的角度讨论了 InheritedWidget 的实现原理。总之,InheritedWidget在Flutter中是一个非常非常重要的组件。