InheritedWidget 详解
在 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;
}
转载自:https://juejin.cn/post/7361687968518488098