简介
业务开发中经常会碰到这样的情况,多个Widget需要同步同一份全局数据,比如点赞数、评论数、夜间模式等等。在安卓中,一般的实现方式是观察者模式,需要开发者自行实现并维护观察者的列表。在flutter中,原生提供了用于Widget间共享数据的InheritedWidget,当InheritedWidget发生变化时,它的子树中所有依赖了它的数据的Widget都会进行rebuild,这使得开发者省去了维护数据同步逻辑的麻烦。
使用范例
先来看下官方注释中的范例:
class FrogColor extends InheritedWidget {
const FrogColor({
Key key,
@required this.color,
@required Widget child,
}) : assert(color != null),
assert(child != null),
super(key: key, child: child);
final Color color;
static FrogColor of(BuildContext context) {
return context.inheritFromWidgetOfExactType(FrogColor);
}
@override
bool updateShouldNotify(FrogColor old) => color != old.color;
}
实现比较简单,直接继承InheritedWidget即可,类中的color即为需要共享的数据。
此外需要实现updateShouldNotify
方法,当InheritedWidget被更新时,该方法会被调用,并传入更新之前的Widget对象,该方法内需要实现新旧数据的比较,若数据不同需要通知依赖的Widget更新,则返回true。
使用时,可以通过FrogColor.of(context).color
获取到它的值。
下面举一个具体的例子,简单的使用场景如下图所示:
将最上层的Widget用InheritedWidget包装起来,并设置颜色为绿色,子Widget中需要使用该颜色时调用FrogColor.of(context).color
来获取,如图中的Icon和Image,此时调用该方法获取到的颜色即为绿色。当InheritedWidget的数据有变化,即通过setState
将绿色改为红色后,依赖了该数据的Icon和Image会自动rebuild,构造新的Widget时,此时从FrogColor.of(context).color
获取到的颜色也会变为红色,由此便实现了数据的同步。
源码分析
InheritedWidget
先来看下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:
abstract class ProxyWidget extends Widget {
const ProxyWidget({ Key key, @required this.child }) : super(key: key);
final Widget child;
}
可以看出Widget内除了实现了createElement
方法外没有其他操作了,它的实现关键一定就是InheritedElement
了。
InheritedElement
class InheritedElement extends ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
@override
InheritedWidget get widget => super.widget;
// 这个Set记录了所有依赖的Element
final Set<Element> _dependents = new HashSet<Element>();
// 该方法会在Element mount和activate方法中调用,_inheritedWidgets为基类Element中的成员,用于提高Widget查找父节点中的InheritedWidget的效率,它使用HashMap缓存了该节点的父节点中所有相关的InheritedElement,因此查找的时间复杂度为o(1)
@override
void _updateInheritance() {
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;
}
// 该方法在父类ProxyElement中调用,看名字就知道是通知依赖方该进行更新了,这里首先会调用重写的updateShouldNotify方法是否需要进行更新,然后遍历_dependents列表并调用didChangeDependencies方法,该方法内会调用mardNeedsBuild,于是在下一帧绘制流程中,对应的Widget就会进行rebuild,界面也就进行了更新
@override
void notifyClients(InheritedWidget oldWidget) {
if (!widget.updateShouldNotify(oldWidget))
return;
for (Element dependent in _dependents) {
dependent.didChangeDependencies();
}
}
}
其中_updateInheritance
方法在基类Element中的实现如下:
void _updateInheritance() {
_inheritedWidgets = _parent?._inheritedWidgets;
}
总结来说就是Element在mount的过程中,如果不是InheritedElement,就简单的将缓存指向父节点的缓存,如果是InheritedElement,就创建一个缓存的副本,然后将自身添加到该副本中,这样做会有两个值得注意的点:
- InheritedElement的父节点们是无法查找到自己的,即InheritedWidget的数据只能由父节点向子节点传递,反之不能。
- 如果某节点的父节点有不止一个同一类型的InheritedWidget,调用
inheritFromWidgetOfExactType
获取到的是离自身最近的该类型的InheritedWidget。
看到这里似乎还有一个问题没有解决,依赖它的Widget是在何时被添加到_dependents
这个列表中的呢?
回忆一下从InheritedWidget中取数据的过程,对于InheritedWidget有一个通用的约定就是添加static的of方法,该方法中通过inheritFromWidgetOfExactType
找到parent中对应类型的的InheritedWidget的实例并返回,与此同时,也将自己注册到了依赖列表中,该方法的实现位于Element类,实现如下:
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
// 这里通过上述mount过程中建立的HashMap缓存找到对应类型的InheritedElement
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
// 这个列表记录了当前Element依赖的所有InheritedElement,用于在当前Element deactivate时,将自己从InheritedElement的_dependents列表中移除,避免不必要的更新操作
_dependencies ??= new HashSet<InheritedElement>();
_dependencies.add(ancestor);
// 这里将自己添加到了_dependents列表中,相当于注册了监听
ancestor._dependents.add(this);
return ancestor.widget;
}
_hadUnsatisfiedDependencies = true;
return null;
}
到此InheritedWidget就分析完了,相比观察者模式,使用它是不是方便了很多呢?