背景
每种语言都有自己数据共享的方式,全局变量、单例、数据持久化等。我们在进行数据的共享的时候,不仅要考虑到数据的共享性,还需考虑到数据变化后,共享的各个组件如何进行数据更新。本篇针对这个问题,介绍一下flutter中数据共享的方式,以及如何进行数据变更之后通知各个组件进行刷新的。
实现
概念
Flutter中已经为我们提供了一个widget来进行数据共享,它就是InheritedWidget
,看下官方对它的介绍:
InheritedWidget
是Flutter中非常重要的一个功能型组件,它提供了一种数据在widget树中从上到下传递、共享的方式,比如我们在应用的根widget中通过InheritedWidget
共享了一个数据,那么我们便可以在任意子widget中来获取该共享的数据!这个特性在一些需要在widget树中共享数据的场景中非常方便!如Flutter SDK中正是通过InheritedWidget来共享应用主题(Theme
)和Locale (当前语言环境)信息的
从官方解释来看,InheritedWidget
是一个从上往子树和节点上传递数据,当数据改变的时候,通知子widgets实现数据变化后更新。
使用
我们通过官方栗子来看下它具体的工作模式:
1.首先我们定义一个用来数据共享的widget,继承自InheritedWidget:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
@required this.data,
Widget child
}) :super(child: child);
final int data; //需要在子树中共享的数据
//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ShareDataWidget);
}
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget
@override
bool updateShouldNotify(ShareDataWidget old) {
//如果返回true,则子树中依赖(build函数中有调用)本widget
//的子widget的`state.didChangeDependencies`会被调用
return old.data != data;
}
}
2.然后我们实现一个用来显示从ShareDataWidget中共享的数据:
class _TestWidget extends StatefulWidget {
@override
__TestWidgetState createState() => new __TestWidgetState();
}
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
//使用InheritedWidget中的共享数据
return Text(ShareDataWidget
.of(context)
.data
.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
//如果build中没有依赖InheritedWidget,则此回调不会被调用。
print("Dependencies change");
}
}
3.定义一个按钮,点击自增,来看当InheritedWidget的数据发生变化时,子widget中进行数据当更新:
class InheritedWidgetTestRoute extends StatefulWidget {
@override
_InheritedWidgetTestRouteState createState() => new _InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget( //使用ShareDataWidget
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: _TestWidget(),//子widget中依赖ShareDataWidget
),
RaisedButton(
child: Text("Increment"),
//每点击一次,将count自增,然后重新build,ShareDataWidget的data将被更新
onPressed: () => setState(() => ++count),
)
],
),
),
);
}
}
运行后,我们会发现,每点击一下自增按钮,_TestWidget中会更新count的显示。
原理
我们介绍完了InheritedWidget的用法,接下来我们看一下,它是如何实现数据共享以及数据变化之后,它是如何通知子widget进行数据更新的呢?
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);
}
这里涉及到widget的didChangeDependencies概念:
在
StatefulWidget
里,State
对象有一个didChangeDependencies
回调,它会在“依赖”发生变化时被Flutter Framework调用。而这个“依赖”指的就是子widget是否使用了父widget中InheritedWidget
的数据!如果使用了,则代表子widget依赖有依赖InheritedWidget
;如果没有使用则代表没有依赖。这种机制可以使子组件在所依赖的InheritedWidget
变化时来更新自身!比如当主题、locale(语言)等发生变化时,依赖其的子widget的didChangeDependencies
方法将会被调用。
当updateShouldNotify返回true时,即子widget的“依赖”发生了改变,于是回调了子widget的didChangeDependencies方法。一般来说,子widget很少会重写此方法,因为在依赖改变后framework也都会调用build()
方法。但是,如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以避免每次build()
都执行这些昂贵操作。
从源码中看,InheritedWidget的实现很简单:
- createElement:创建对应widget的Element,所有widget共有的一个方法。
- updateShouldNotify:从名字看就可以知道,该方法来决定是否通知子widget进行更新,返回true则通知子widget进行更新
似乎没有看到跟数据共享的身影,既然widget中没有,那一定是在它对应的Element中了。
class InheritedElement extends ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
@override
InheritedWidget get widget => super.widget;
final Map<Element, Object> _dependents = HashMap<Element, Object>();
@override
void _updateInheritance() {
assert(_active);
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
}
...
@protected
void updateDependencies(Element dependent, Object aspect) {
setDependencies(dependent, null);
}
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}
@override
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget);
}
@override
void notifyClients(InheritedWidget oldWidget) {
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (Element dependent in _dependents.keys) {
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));
notifyDependent(oldWidget, dependent);
}
}
}
从InheritedElement源码中,我们可以看到,InheritedElement维护了一个依赖列表。它记录了所有依赖它的Element,然后通过notifyClients方法对依赖列表中的每一个Element进行更新,即调用子widget的didChangeDependencies()方法,notifyClients在其父类中当widget改变的时候调用。这就解释了,当InheritedWidget状态改变更新时,如何通知依赖其的子widget。还有一个问题,子widget是什么时候被加入到这个依赖列表的呢?
还记得我们刚刚举的栗子吗,在创建共享widget的时候我们写了一个通用约定的静态方法:
//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ShareDataWidget);
}
会不会跟它有关呢,我们来看下该方法的实现:
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
@override
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
_inheritedWidgets:在InheritedElement源码中,可以看到通过_updateInheritance方法, 该map缓存了该节点的父节点中所有相关的InheritedElement。
可以看到,我们是通过inheritFromWidgetOfExactType()静态方法,将子widget注册给InheritedWidget,这就是子widget“依赖”的由来。需注意的是:inheritFromWidgetOfExactType是获取最近的给定类型的Widget,该widget必须是InheritedWidget的子类,并向该widget注册传入的context,当该widget改变时,这个context会重新构建以便从该widget获得新的值。
至此,我们分析完了子widget是如何从注册“依赖”,到当InheritedWidget数据发生变化时,如何通知子widget进行数据更新的过程。