在 Flutter
中,我们经常会看到这样的代码: Theme.of(context)
, MediaQuery.of(context)
,这背后其实就和我们今天的主角 InheritedWidget
有关。
介绍
InheritedWidget
是 Flutter
中的一个非常重要的功能性 widget
,适用于在 widget
树中共享数据的场景。通过它,可以方便地将数据在 widget
树不同层级间进行传递。
Base class for widgets that efficiently propagate information down the tree.
基本使用
我们先看官方给的例子。
class FrogColor extends InheritedWidget {
const FrogColor({
super.key,
required this.color,
required super.child,
});
final Color color;
static FrogColor? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<FrogColor>();
}
static FrogColor of(BuildContext context) {
final FrogColor? result = maybeOf(context);
assert(result != null, 'No FrogColor found in context');
return result!;
}
@override
bool updateShouldNotify(FrogColor oldWidget) => color != oldWidget.color;
}
FrogColor
可以理解为数据仓库,通过继承 InheritedWidget
,将要共享的数据保存在 color
属性中,并提供一个 of
方法方便子 widget
通过 widget
树找到它。代码中重写的 updateShouldNotify
方法,决定当共享的数据发生变化时,是否通知依赖 color
的子 widget
重新 build
。
接着我们看看子 widget
中如何获取 color
。
class TestInheritedWidget extends StatelessWidget {
const TestInheritedWidget({super.key});
@override
Widget build(BuildContext context) {
return FrogColor(
color: Colors.green,
child: Builder(
builder: (BuildContext innerContext) {
return Text(
'Hello Frog',
style: TextStyle(color: FrogColor.of(innerContext).color),
);
},
),
);
}
}
FrogColor
中的 Text
通过 of
方法就可以找到 color
。需要注意的是,context
必须是 InheritedWidget
的后代,这意味着必须处于 InheritedWidget
树中。上述代码中,context
来自 Builder
,而 Builder
是 FrogColor
的子 widget
,因此可以找到 color
。
// ...
return FrogColor(
color: Colors.green,
child: Text(
'Hello Frog',
style: TextStyle(color: FrogColor.of(innerContext).color),
),
);
// ...
如果去掉 Builder
,则使用的是 TestInheritedWidget
的 context
,会抛出 No FrogColor found in context 异常。
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building TestInheritedWidget(dirty):
No FrogColor found in context
'package:flutter_wiki/inherit_widget.dart':
Failed assertion: line 18 pos 12: 'result != null'
上面的是只读的情况,接下来,我们看看修改数据的情况。
class FrogWidget extends InheritedWidget {
const FrogColor({
super.key,
required this.color,
required super.child,
required this.onColorChange,
});
final Color color;
final VoidCallback onColorChange;
// ...
}
我们给 FrogWidget
增加一个 onColorChange
的方法,同时让TestInheritedWidget
继承 StatefulWidget
。
class TestInheritedWidget extends StatefulWidget {
const TestInheritedWidget({super.key});
@override
State<TestInheritedWidget> createState() => _TestInheritedWidgetState();
}
class _TestInheritedWidgetState extends State<TestInheritedWidget> {
Color color = Colors.green;
void onColorChange() {
setState(() {
color = Colors.red;
});
}
@override
Widget build(BuildContext context) {
return FrogColor(
color: color,
onColorChange: onColorChange,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Builder(
builder: (BuildContext innerContext) => Text(
'Hello Frog',
style: TextStyle(color: FrogColor.of(innerContext).color),
),
),
ElevatedButton(
onPressed: () => onColorChange(),
child: const Text("Change Color"),
),
],
),
);
}
}
当点击按钮时,会发现 Hello Frog 的颜色变为红色了。
在上述例子中,不管 FrogColor
中的 updateShouldNotify
是否返回 true
,都会触发更新。因为执行了 setState
之后,build
方法会重新执行,Text
中直接使用最新的 color
。我们需要 Text
单独抽出来成 class
类 widget
。
class ColoredWidget extends StatefulWidget {
const ColoredWidget({super.key});
@override
State<ColoredWidget> createState() => _ColoredWidgetState();
}
class _ColoredWidgetState extends State<ColoredWidget> {
@override
Widget build(BuildContext context) {
return Text(
'Hello Frog',
style: TextStyle(color: FrogColor.of(context).color),
);
}
}
// ...
return FrogColor(
// ...
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 子 widget
const ColoredWidget(),
ElevatedButton(
// ..
),
],
),
);
// ...
这时,如果手动修改 shouldUpdateNotify
返回 false
,ColoredWidget
就不会更新了。
深入了解
我们回头在看看这行代码:FrogColor.of(context).color
。
- 在这行代码中,我们获取到位于
widget
树顶部的FrogColor
的引用 - 然后,当我们书写
.of(context)
时,框架会将widget
(这里是ColoredWidget
)会注册为FrogColor
的侦听器 - 因此,每当
FrogColor
中的值发生变化时,依赖它的widget
就会被重建
从 InheritedWidget
访问数据时需要遵循的一些原则:
- 在
StatefulWidget
中,不能在initState
中调用.of(context)
方法,会直接报错 - 可以在
build
,didChangeDependencies
,didUpdate
等钩子函数中调用.of(context)
class _ColoredWidgetState extends State<ColoredWidget> {
late Color textColor;
@override
void didChangeDependencies() {
super.didChangeDependencies();
textColor = FrogColor.of(context).color;
}
@override
Widget build(BuildContext context) {
return Text(
'Hello Frog',
style: TextStyle(color: textColor),
);
}
}
在子 widget
中,我们已经知道了该在哪里调用 .of(context)
方法,接下来我们来看看它是如何工作的。
要回答这个问题,我们需要先理解 didChangeDependencies
的“依赖”变化指的是什么依赖?
我们来看一下源码。
/// Called when a dependency of this [State] object changes.
///
/// For example, if the previous call to [build] referenced an
/// [InheritedWidget] that later changed, the framework would call this
/// method to notify this object about the change.
///
/// This method is also called immediately after [initState]. It is safe to
/// call [BuildContext.dependOnInheritedWidgetOfExactType] from this method.
///
/// Subclasses rarely override this method because the framework always
/// calls [build] after a dependency changes. Some subclasses do override
/// this method because they need to do some expensive work (e.g., network
/// fetches) when their dependencies change, and that work would be too
/// expensive to do for every build.
@protected
@mustCallSuper
void didChangeDependencies() { }
从源码中,可以清楚地看到,这里的依赖指的是:是否使用了父 widget
中 InheritedWidget
的数据。如果使用了,则表示子 widget
存在依赖,反之就没有。这种机制可以保证是子 widget
在依赖发生变化时进行更新。
那是在哪里注册了依赖关系呢?我们来看 dependOnInheritedWidgetOfExactType
的源码。
/// Once a widget registers a dependency on a particular type by calling this
/// method, it will be rebuilt, and [State.didChangeDependencies] will be
/// called, whenever changes occur relating to that widget until the next time
/// the widget or one of its ancestors is moved (for example, because an
/// ancestor is added or removed).
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
上面是 dependOnInheritedWidgetOfExactType
的声明,从注释(删减了部分注释)可以看到,widget
通过调用此方法注册了依赖关系,当依赖发生变化时,调用 State.didChangeDependencies
回调。
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
if (ancestor != null) {
/// 这里注册了依赖关系
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
上面 dependOnInheritedWidgetOfExactType
的实现,当祖先元素存在 InheritedElement
时,注册依赖关系。
到这里我们总算弄清楚了 InheritedWidget
的原理了,总结一下:
- 当在子
widget
中调用.of(context)
时,会触发dependOnInheritedWidgetOfExactType
函数进行依赖注册 - 当依赖发生变化时,重建子
widget
,并且也会调用didChangeDependencies
回调
注意:
- 前面说过,我们也可以在
build
中调用.of(context)
方法,与在didChangeDependencies
中调用相比,性能差异不会那么显著,但还是推荐在didChangeDependencies
中进行调用。 - 子
widget
很少重写didChangeDependencies
方法,因为依赖有变化,Flutter
框架会重新build
。但如果需要再依赖变化后执行一些昂贵的操作如网络请求,最好是在该方法中执行,避免每次build
时执行昂贵的操作。
最后,我们来想一个问题,如果我们只想子 widget
获取 InheritedWidget
中的数据,但又不想数据变化时更新子 widget
,这时该怎么办呢?
除了前面所说,将 updateShouldNotify
直接返回 false
,我们还可以再访问 InheritedWidget
数据时不建立依赖关系。在实现 of(contex)
方法时,改为调用 getElementForInheritedWidgetOfExactType
方法。
class FrogColor extends InheritedWidget {
// ...
static FrogColor? maybeOf(BuildContext context) {
// return context.dependOnInheritedWidgetOfExactType<FrogColor>();
return context.getElementForInheritedWidgetOfExactType<FrogColor>()!.widget
as FrogColor;
}
// ...
}
与 dependOnInheritedWidgetOfExactType
相比, getElementForInheritedWidgetOfExactType
没有进行依赖注册。
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
if (ancestor != null) {
/// 这里注册了依赖关系
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
return ancestor;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。