Flutter 理解数据共享组件 InheritedWidget
InheritedWidget 是Flutter中非常重要的一个功能型组件,它提供了一种数据在widget 树中从上到下传递、共享的方式,比如我们在应用的根 widget 中通过 InheritedWidget 共享了一个数据,那么我们便可以在任意子 widget 中来获取该共享的数据!这个特性在一些需要在widget树中共享数据的场景中非常方便!如 Flutter SDK中 正是通过 InheritedWidget 来共享应用主题(Theme)和语言环境 (Locale)信息的。
先来看一个案例,下面的案例也可以实现 Flutter 的官方 demo,点击按钮实现数字增加的操作,不同的是这里实现的是点击父组件上面的按钮,子组件上面的数据进行变化,这里就是使用了 InheritedWidget 来实现父子组件的数据共享操作
class InheritedDemo extends InheritedWidget {
final int data; //需要在子树中共享的数据,保存点击次数
InheritedDemo(this.data, {Widget child}) : super(child: child);
//定义一个便捷方法,方便子树中的widget获取共享数据
static InheritedDemo of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<InheritedDemo>();
}
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget
@override
bool updateShouldNotify(InheritedDemo old) {
//如果返回true,则子树中依赖(build函数中有调用)本widget
//的子widget的`state.didChangeDependencies`会被调用
return old.data != data;
}
}
子组件代码,主要是获取共享数据并展示
class InheritedTest extends StatefulWidget {
@override
_InheritedTestState createState() => new _InheritedTestState();
}
class _InheritedTestState extends State<InheritedTest> {
@override
Widget build(BuildContext context) {
//使用InheritedWidget中的共享数据,如果这里没有依赖 InheritedDemo 的话再怎么点击按钮这里的 didChangeDependencies 也不会被调用
return Text(InheritedDemo.of(context).data.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
//如果build中没有依赖InheritedWidget,则此回调不会被调用。
print("Dependencies change");
}
}
父组件,这里主要进行数据的操作
class InheritedTestWidget extends StatefulWidget {
@override
_InheritedTestWidgetState createState() => new _InheritedTestWidgetState();
}
class _InheritedTestWidgetState extends State<InheritedTestWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('测试'),
),
body: Center(
//外层使用 Inherited 组件进行包裹,并设置共享数据字段
child: InheritedDemo(
count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: InheritedTest(),//子 widget 中依赖 InheritedDemo
),
RaisedButton(
child: Text("点我"),
//每点击一次,将count自增,然后重新build, InheritedDemo 的 data 将被更新
onPressed: () => setState(() => ++count),
)
],
),
),
),
);
}
}
注意这里的重点,为什么这样就能让子组件获取值了呢?最主要的是子组件中获取数据的那句代码,来看一下具体实现
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
//注意这里
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
来看一下 dependOnInheritedElement
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
可以看到 dependOnInheritedElement 方法中主要是注册了依赖关系!看到这里也就清晰了,调用 dependOnInheritedWidgetOfExactType() 会注册依赖关系,所以在调用 dependOnInheritedWidgetOfExactType() 时,InheritedWidget 和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的 didChangeDependencies() 方法和 build() 方法。
如果子组件只是想获取 值,并不想子组件调用 didChangeDependencies ,我们只需要吧 dependOnInheritedWidgetOfExactType() 换为 getElementForInheritedWidgetOfExactType() 即可,唯一的区别就是没有注册子组件的依赖关系。
但是注意,如果将上面示例中 InheritedDemo.of() 方法实现改成调用 getElementForInheritedWidgetOfExactType(),运行示例后,点击 "点我" 按钮,发现子组件的didChangeDependencies() 方法确实不会再被调用,但是其 build() 仍然会被调用!造成这个的原因其实是,点击 "点我" 按钮后,会调用父组件的 setState() 方法,此时会重新构建整个页面,由于示例中,子组件并没有任何缓存,所以它也都会被重新构建,所以也会调用 build() 方法。
那么,现在就带来了一个问题:实际上,我们只想更新子树中依赖了 InheritedDemo 的组件,而现在只要调用父组件的setState()方法,所有子节点都会被重新 build,这很没必要,那么有什么办法可以避免呢?答案是缓存!一个简单的做法就是通过封装一个 StatefulWidget,将子Widget树缓存起来。