目前,开发的flutter项目中,状态管理库使用是Provider,Provider 基于 InheritedWidget 组件封装,想要减少日常开发采坑,就不得不去了解 InheritedWidget 组件的工作原理。由于要从源码角度分析 InheritedWidget 组件的工作原理,在阅读本文前,最好对 flutter 的知识有一定了解,这样才能更好的了解,本文所要表达的意思。
- 熟悉
flutter基本使用。 - 了解
provider的框架。 - 了解
Widget和Element之间关系。 - 了解
Element在flutter渲染时方法的调用。
一、 InheritedWidget
本文中用到的生产环境
1.1 InheritedWidget 简述
/// 有效地沿树向下传播信息的 Widget 的基类,子 Widget 要想获取最近特定类型的 InheritedWidget实例,请使用
/// [BuildContext.dependOnInheritedWidgetOfExactType]。子 Widget 以这种方式引用时,当 InheritedWidget 改变状态
/// 时,会重新构建依赖的子 Widget。
abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ Key key, Widget child })
: super(key: key, child: child);
@override
InheritedElement createElement() => InheritedElement(this);
@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
InheritedWidget 代码很简单,主要有两个方法:
createElement生成InheritedElement对象。updateShouldNotify用于控制当前InheritedWidget发生变化, 所依赖的Widget是否需要重建。
1.1.1 BuildContext
从 InheritedWidget 描述可得知,如果子 Widget 需要获取 InheritedWidget 对象,可以通过 BuildContext.dependOnInheritedWidgetOfExactType 获取。看下 BuildContext 类的 dependOnInheritedWidgetOfExactType 。
abstract class BuildContext {
/// 获取给定类型“T”的最近 widget,它必须是具体 [InheritedWidget] 子类的类型,并将此构建上下文注册到该 widget,以便当该 widget 更改时(或引入该类型的新 widget, 或 widget 消失),此构建上下文将被重建,以便它可以从该 widget 获取新值。
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect });
/// 获取与给定类型“T”的最近 widget 对应的 element,该 element 必须是具体 [InheritedWidget] 子类的类型。 如果找不到这样的 element,则返回 null。
/// 调用这个方法是 O(1) 一个小的常数因子。 此方法不会像 [dependOnInheritedWidgetOfExactType] 那样与目标建立关
/// 系。 不应从 [State.dispose] 调用此方法,因为此时 element 树不再稳定。 要从该方法引用祖先,请通过在
/// [State.didChangeDependencies] 中调用 [dependOnInheritedWidgetOfExactType] 来保存对祖先的引用。 使用
/// [State.deactivate] 中的此方法是安全的,每当 widget 从树中移除时都会调用该方法。
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();
}
1.2 InheritedWidget 应用
接下来,我们写一个基于 InheritedWidget 组件实现的 计数器 。
1.2.1 CountScope
class CountScope extends InheritedWidget {
const CountScope({this.count, Widget child}) : super(child: child);
final int count;
static CountScope of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CountScope>();
}
@override
bool updateShouldNotify(CountScope oldWidget) {
return oldWidget.count != count;
}
}
CountScope 继承 InheritedWidget
of,子Widget获取CountScope对象。[见1.1小节]updateShouldNotify当oldWidget.count != count刷新依赖的widget。[见1.1小节]
1.2.2 CountWidget
class CountWidget extends StatefulWidget {
const CountWidget({Key key}) : super(key: key);
@override
_CountWidgetState createState() => _CountWidgetState();
}
class _CountWidgetState extends State<CountWidget> {
@override
Widget build(BuildContext context) {
print('_CountWidgetState build');
final int count = CountScope.watch(context).count;
return Container(child: Text('$count', style: Theme.of(context).textTheme.headline4));
}
}
CountWidget 调用 CountScope.of(context).count 显示计数结果。
1.2.3 MyHomePage
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
print('_MyHomePageState build');
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
CountScope(count: _counter, child: CountWidget()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
到这里计数器功能差不多就完成了,当我们点击浮动 + 按钮时,计数器的数值会累加,最后可以看到效果就是这样。
1.2.4 updateShouldNotify
我们在 1.1 小节 提过, updateShouldNotify 返回 true ,表示更新依赖的 Widget,false 不更新,现在让我们验证下。修改 CountScope 代码,当 count < 3,才能更新依赖的 widget。
class CountScope extends InheritedWidget {
const CountScope({this.count, Widget child}) : super(child: child);
final int count;
static CountScope watch(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CountScope>();
}
@override
bool updateShouldNotify(CountScope oldWidget) {
//return oldWidget.count != count;
print('count:$count');
return count < 3;
}
}
结果确实如我们所料那样,不停的点击 + 按钮,界面上的数字一直显示的是 2。认真观察 log 会发现,实际上 count 是会一直累加。
讲到这里,我们就会有很多疑问?为什么 CountWidget 能获取到 CountScope 中 count 的值?又为什么CountScope 中 count 数值有变化,但是当 count > 3 时,CountWidget 界面却没有更新呢?
遇事不决,看源码
二、Widget 更新流程
我们知道调用 setState 后,把当前 element 标记为 dirty, 当下一次 vsync 信号到来的时候,回调执行 handleBeginFrame 和 handleDrawFrame ,然后经过一些列的调用,最后会调用 BuildOwner.buildScope ,遍历 _dirtyElements 集合,调用 Element 的 rebuild 刷新组件。
本文不会完整介绍
Widget的更新机制,有兴趣的同学,可以自己去了解下。(接下来源码片段,都只保留关键代码)。
2.1 类图
2.2 时序图
2.3 Element#rebuild
前面,我们提到调用 setState 后 ,经过一系列的调用,最终调用 Element 的 rebuild。
abstract class Element extends DiagnosticableTree implements BuildContext {
/// 当调用 [BuildOwner.scheduleBuildFor] 会将该 Element 标记为 dirty,
/// 当 Element 首次 build 时会被 [mount] 调用,当 widget 更改时由 [update] 调用。
void rebuild() {
if (!_active || !_dirty)
return;
performRebuild();
}
/// 在进行适当的检查后由 rebuild() 调用。
@protected
void performRebuild(); /// [见2.4小节]
}
2.4 ComponentElement#performRebuild
abstract class ComponentElement extends Element {
/// 调用 StatelessWidget 对象的 StatelessWidget.build 方法(对于无状态小部件)
/// 或 State 对象的 State.build 方法(对于有状态小部件),然后更新 widget 树。
/// 在 mount 期间自动调用以生成第一个构建,并在 element 需要更新时通过 rebuild 调用。
@override
void performRebuild() {
Widget built;
try {
built = build();
} finally {
/// ...
_dirty = false;
}
try {
_child = updateChild(_child, built, slot); /// [见2.5小节]
} catch (e, stack) {
// ...
_child = updateChild(null, built, slot);
}
/// 子类应该覆盖这个函数来为它们的小部件实际调用适当的 `build` 函数
///(例如,[StatelessWidget.build] 或 [State.build])。
@protected
Widget build();
}
2.5 Element#updateChild
abstract class Element extends DiagnosticableTree implements BuildContext {
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget); /// [见2.6小节]
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
newChild = inflateWidget(newWidget, newSlot); /// [见2.5.1小节]
}
return newChild;
}
}
2.5.1 Element#inflateWidget
如果第一次调用 updateChild,默认 child = null,就会执行 inflateWidget,生成 Element newChild 对象,最后把 newChild 赋值给 _child [2.4小节],后续就使用 newChild 传入 updateChild。
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
// ...
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);// [见2.5.2小节]
return newChild;
}
调用 widget.createElement,生成 Element 对象,然后调用 Element 的 mount,递归调用,完成所有子 widget 的刷新。
2.5.2 Element#mount
void mount(Element parent, dynamic newSlot) {
//...
final Key key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();
}
void _updateInheritance() {
_inheritedWidgets = _parent?._inheritedWidgets;
}
将父类中的 _inheritedWidgets 集合对象,传到子类。
2.6 Element#update
[2.5.1] [2.5.2] 两个小节,描述第一次加载 widget 过程。接下来介绍 child.update(newWidget) 。
@mustCallSuper
void update(covariant Widget newWidget) { /// [见2.7小节]
_widget = newWidget;
}
通过查看 [2.1] 类图,发现最下层的子类是 InheritedElement。
2.7 InheritedElement#update
class InheritedElement extends ProxyElement {
@override
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget); /// [见2.8小节]
}
}
widget.updateShouldNotify(oldWidget) 这个方法是不是似曾相识,这个就是一开始,我们说的 InheritedWidget.updateShouldNotify [见1.2.4小节],这里也验证之前说法,如果 true 表示会继续后续的,false 不执行后续的操作。我们接着看 super.updated 后面的流程。
2.8 ProxyElement#update
通过查看 [2.1] 类图, InheritedElement 继承自 ProxyElement。
abstract class ProxyElement extends ComponentElement {
@override
ProxyWidget get widget => super.widget as ProxyWidget;
@override
Widget build() => widget.child;
/// ...
@override
void update(ProxyWidget newWidget) {
final ProxyWidget oldWidget = widget;
/// ...
updated(oldWidget);
_dirty = true;
rebuild();
}
}
/// 当 widget 更改时, 在 build 期间调用。
/// 默认情况下,调用 notifyClients。 子类可以覆盖此方法以避免不必要地调用 notifyClients(如果旧的和新的小部件是等效的)。
@protected
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}
/// 通知其他对象与此 Elment 关联的 Widget 已更改。
/// 在更改与此元素关联的 Widget 之后但在重建此元素之前,在update期间(通过updated )调用
@protected
void notifyClients(covariant ProxyWidget oldWidget); // [见2.9小节]
2.9 InheritedElement#notifyClients
class InheritedElement extends ProxyElement {
final Map<Element, Object> _dependents = HashMap<Element, Object>();
/// [2.5.2小节] 初始化时调用
@override
void _updateInheritance() {
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;
}
/// 返回使用 [setDependencies]为 [dependent] 记录的依赖项值。
/// 每个依赖元素都映射到单个对象值,该值表示元素如何依赖此 [InheritedElement]。
/// 默认情况下,此值为 null,并且默认情况下会无条件重建相关元素。
/// 子类可以使用 [updateDependencies] 管理这些值
/// 以便他们可以选择性地重建 [notifyDependent] 中的依赖项。
/// 此方法通常仅在 [updateDependencies] 的覆盖中调用。
/// 也可以看看:
/// [updateDependencies],每次使用 [dependOnInheritedWidgetOfExactType] 创建依赖项时都会调用它。
/// [setDependencies],设置依赖元素的依赖值。
/// [notifyDependent],可以覆盖它以使用依赖项的依赖项值来决定是否需要重建依赖项。
/// [InheritedModel],这是一个使用该方法管理依赖值的类的例子。
@protected
Object getDependencies(Element dependent) {
return _dependents[dependent];
}
/// 为dependent设置getDependencies值返回的值。
/// 每个依赖元素都映射到单个对象值,该值表示元素如何依赖此InheritedElement 。
/// [updateDependencies]方法在默认情况下将该值设置为null,以便无条件地重建依赖元素。
/// 子类可以使用[updateDependencies]管理这些值,以便它们可以有选择地在[NotifyDependency]中重建依赖项。
/// 此方法通常仅在updateDependencies覆盖中调用。
@protected
void setDependencies(Element dependent, Object value) {
_dependents[dependent] = value;
}
/// 添加新的 [dependent] 时由 [dependOnInheritedWidgetOfExactType] 调用。
/// 每个依赖元素都可以映射到单个对象值 [setDependencies]。 此方法可以使用 [getDependencies] 查找现有依赖项。
/// 默认情况下,此方法将 [dependent] 的继承依赖项设置为 null。 这仅用于记录对 [dependent] 的无条件依赖。
/// 子类可以管理自己的依赖项值,以便它们可以在 [notifyDependent] 中重建依赖项。
/// [getDependencies],返回依赖项的当前值元素。
/// [setDependencies],设置依赖元素的值。
/// [notifyDependent],可以覆盖它以使用依赖的依赖值来决定是否需要重建依赖。
/// [InheritedModel],这是一个使用该方法的类的例子管理依赖值。
@protected
void updateDependencies(Element dependent, Object aspect) {
setDependencies(dependent, null);
}
/// 由 [notifyClients] 为每个依赖调用。
/// 默认情况下调用 [dependent.didChangeDependencies()] 。
/// 子类可以覆盖此方法以根据 [getDependencies] 的值有选择地调用 [didChangeDependencies] 。
/// 也可以看看:
/// updateDependencies ,每次使用 [dependOnInheritedWidgetOfExactType] 创建依赖项时都会调用它。
/// getDependencies ,它返回依赖元素的当前值。
/// setDependencies ,它设置依赖元素的值。
/// InheritedModel ,这是使用此方法管理依赖项值的类的示例。
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies(); /// [见2.9.1]
}
/// 通过调用Element.didChangeDependencies通知所有依赖的Elements,这个widget已经更改。
/// 此方法只能在 build 阶段调用。 通常当 Inherited widget 被重建时,这个方法会自动调用,例如,作为在 Inherited widget上方
/// 调用State.setState的结果
@override
void notifyClients(InheritedWidget oldWidget) {
for (final Element dependent in _dependents.keys) {
/// ...
notifyDependent(oldWidget, dependent);
}
}
}
2.9.1 Element#didChangeDependencies
void didChangeDependencies() {
markNeedsBuild();
}
看到 markNeedsBuild 是不是很熟悉。没错,就是我们经常使用 setState,调用一样的方法。
void setState(VoidCallback fn) {
// ...
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
上面我们发现 notifyClients,遍历循环 _dependents.keys,调用 Element.didChangeDependencies 更新依赖。我们看下 dependents 是怎么来的呢?我们看 updateDependencies 注释,是调用 context.dependOnInheritedWidgetOfExactType 添加 Element。
2.10 Element#dependOnInheritedWidgetOfExactType
Element是BuildContext具体实现类
@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
2.11 流程图
下图,只是简单画了 InheritedElement 更新依赖组件的大致流程图,方便大家吸收,具体的细节还需要自己去挖掘。
2.12 小结
dependOnInheritedWidgetOfExactType 通过 inheritedWidgets[T] 获取到对应的 InheritedElement,当 InheritedElement不为空,接着调用 InheritedElement 的 updateDependencies,把当前 Element 注入到要获取 InheritedElement 的 _dependents.keys 集合,接着返回 InheritedElement 的 widget 对象。当InheritedWidget 更新时,先通过 updateShouldNotify 判断当前 InheritedElement 是否能进行 updated,当值为 true 时,通过循环遍历 _dependents.keys 集合,来更新所有依赖的 widget 。
三、自定义 Provider
前面写了一个简单的 计数器,同时也介绍了 InheritedWidget 更新依赖 widget 的原理分析。那我们能不能基于 InheritedWidget 实现一个自己的 Provider 功能呢?接下来,我们基于之前的 计数器 的代码,一步一步优化,实现一个我们自己的 Provider。
3.1 优化方案(一)
通过上面我们能发现,在主页面直接调用 setState,是比较消耗性能,应该把 incrementCounter 操作剥离出去,单独一个 model,这样似乎更符合实际的开发。
3.1.1 CountModel
新建 CountModel 用来进行数据处理及页面更新操作,有点类似 android 中 viewModel。
class CountModel with ChangeNotifier {
int _count = 0;
int get count => _count;
void incrementCounter() {
_count++;
notifyListeners();
}
}
3.1.2 CountProvider
初始化传入数据源 T, 继承 ChangeNotifier ,监听 T notifyListeners 时,刷新 CountProvider,顺便把 dependOnInheritedWidgetOfExactType 从 CountScope 移到 CountProvider 类中。
typedef WidgetBuilder = Widget Function(BuildContext context);
class CountProvider<T extends ChangeNotifier> extends StatefulWidget {
const CountProvider({
Key key,
this.value,
this.builder,
}) : super(key: key);
final T value;
final WidgetBuilder builder;
static T of<T extends ChangeNotifier>(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CountScope<T>>().value;
}
@override
_CountProviderState<T> createState() => _CountProviderState<T>();
}
class _CountProviderState<T extends ChangeNotifier> extends State<CountProvider<T>> {
void _changeValue() {
setState(() {});
}
@override
void initState() {
super.initState();
widget.value?.addListener(_changeValue);
}
@override
void dispose() {
widget.value?.removeListener(_changeValue);
super.dispose();
}
@override
void didUpdateWidget(covariant CountProvider<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
oldWidget.value?.removeListener(_changeValue);
}
widget.value?.addListener(_changeValue);
}
@override
Widget build(BuildContext context) {
print('_CountProviderState build');
return CountScope<T>(
value: widget.value,
child: Builder(
builder: (BuildContext context) {
return widget.builder(context);
},
),
);
}
}
3.1.3 CountScope
class CountScope<T extends ChangeNotifier> extends InheritedWidget {
const CountScope({@required this.value, Widget child}) : super(child: child);
final T value;
@override
bool updateShouldNotify(CountScope<T> oldWidget) {
return true;
}
}
3.1.4 CountWidget
class CountWidget extends StatefulWidget {
const CountWidget({Key key}) : super(key: key);
@override
_CountWidgetState createState() => _CountWidgetState();
}
class _CountWidgetState extends State<CountWidget> {
@override
Widget build(BuildContext context) {
print('_CountWidgetState build');
return Container(
child: Text(
'${CountProvider.of<CountModel>(context).count}',
style: Theme.of(context).textTheme.headline4,
),
);
}
}
3.1.5 MyHomePage
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print('_MyHomePageState build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
CountProvider<CountModel>(
value: CountModel(),
builder: (BuildContext context) {
return Column(children: [
const CountWidget(),
ClickButton(),
]);
},
),
],
),
),
);
}
}
3.1.6 ClickButton
class ClickButton extends StatelessWidget {
const ClickButton({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('ClickButton build');
return ElevatedButton(
onPressed: () {
print('--- 点击 ---');
CountProvider.of<CountModel>(context).incrementCounter();
},
child: const Icon(Icons.add),
);
}
}
当我们点击 + 时,能正常更新数据,我们看 log 会发现, MyHomePage 页面没有重新进行 build,如果我们在MyHomePage 的Column 组件里面,再添加一个不依赖 CountProvider 的组件,会发生什么?
3.1.7 Nothing
class Nothing extends StatefulWidget {
const Nothing({Key key}) : super(key: key);
@override
_NothingState createState() => _NothingState();
}
class _NothingState extends State<Nothing> {
@override
Widget build(BuildContext context) {
print('_NothingState build');
return Container(
child: const Text('我是一个不依赖的 widget'),
);
}
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print('_MyHomePageState build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
CountProvider<CountModel>(
value: CountModel(),
builder: (BuildContext context) {
return Column(children: [
const CountWidget(),
Nothing(), /// 新增
ClickButton(),
]);
},
),
],
),
),
);
}
}
结果如下图:
可以发现,每次我们点击 + ,调用 CountModel 进行 notifyListeners 更新时,都会导致 CountProvider 的子 Widget 全部刷新,不管是否依赖。
3.1.8 小结
现在我们会发现有三个问题:
- 当前
Widget调用CountModel方法时,可以不进行依赖绑定? - 如果我们点击
Flutter Hot Reload按钮,会发现计数器的1,会变成0,这合理吗? - 有没有只会更新依赖的
widget,不更新InheritedElement组件下,所有的子widget?
3.2 优化方案(二)
3.2.1 InheritedElement
因为我们是在 CountProvider 中监听 CountScope 的数据源变化,调用 setState 刷新整个 CountProvider,导致子 widget 也都全部更新,所以,我们应该把数据源更新监听移到 CountScope,让 CountScope 通知刷新依赖的 widget 比较合理。
class CountScope<T extends ChangeNotifier> extends InheritedWidget {
const CountScope({@required this.value, Widget child}) : super(child: child);
final T value;
@override
bool updateShouldNotify(CountScope<T> oldWidget) {
/// 因为是 InheritedElement 进行刷新,所以,这里可以设置为 false
return false;
}
@override
CountScopeElement<T> createElement() {
return CountScopeElement<T>(value, this);
}
}
class CountScopeElement<T extends ChangeNotifier> extends InheritedElement {
CountScopeElement(this.value, InheritedWidget widget) : super(widget) {
value?.addListener(_handleUpdate);
}
final T value;
void _handleUpdate() {
markNeedsBuild();
}
@override
void unmount() {
value?.removeListener(_handleUpdate);
super.unmount();
}
@override
Widget build() {
notifyClients(widget);
return super.build();
}
}
新建 CountScopeElement 继承 InheritedElement,在 CountScope 的 createElement 返回自定义 CountScopeElement 类。在 CountScopeElement 实现数据源变化监听,在销毁时调用移除监听。
-
_handleUpdate调用markNeedsBuild,这个方法我们应该都很熟悉,也就是setState调用后,调用同样的方法,把CountScopeElement标记为dirty。build调用notifyClients(widget),可能会人有不明白,为什么这里要调用notifyClients? -
通过
[2.8小节]我们可以发现,当CountScopeElement进行build时,其实这里的child没有更新,还是同一个对象。[见2.5小节],当child.widget == newWidget时,是不进行child.update()操作,也就没有后续的一串依赖更新操作。所以,这里要重写build,手动调用内部的notifyClients,通知依赖更新操作。
3.2.2 getElementForInheritedWidgetOfExactType
通过 [1.1小节] 我们知道,获取 InheritedWidget 是 dependOnInheritedWidgetOfExactType,并创建依赖关系。我们通过 getElementForInheritedWidgetOfExactType 获取 InheritedElement,这个过程,不创建依赖关系,似乎能解决需要依赖的问题,我们修改 CountProvider 的 of 如下:
static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
if (listen) {
context.dependOnInheritedElement(inheritedElement);
}
return countScopeElement.value;
}
实现思路,获取到 CountScopeElement ,如果 listen = true 时,则进行依赖更新,最后返回 CountScopeElement.value。
3.2.3 CountProvider
class CountProvider<T extends ChangeNotifier> extends StatefulWidget {
const CountProvider({
Key key,
this.value,
this.builder,
}) : super(key: key);
/// 数据源
final T value;
final WidgetBuilder builder;
static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
if (listen) {
context.dependOnInheritedElement(inheritedElement);
}
return countScopeElement.value;
}
@override
_CountProviderState<T> createState() => _CountProviderState<T>();
}
class _CountProviderState<T extends ChangeNotifier> extends State<CountProvider<T>> {
@override
Widget build(BuildContext context) {
print('_CountProviderState build');
return CountScope<T>(
value: widget.value,
child: Builder(
builder: (BuildContext context) {
return widget.builder(context);
},
),
);
}
}
最终效果是可行的,也解决了 [3.1.8小节] 提出的三个问题。上面的代码看起来似乎没什么问题,如果 _MyHomePageState 在进行 setState 操作,会发现什么事情?
class _MyHomePageState extends State<MyHomePage> {
void _refresh() {
setState(() {});
}
@override
Widget build(BuildContext context) {
print('_MyHomePageState build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
CountProvider<CountModel>(
value: CountModel(),
builder: (BuildContext context) {
return Column(children: [
const CountWidget(),
Nothing(),
ElevatedButton(
onPressed: () {
print('--- 点击 ---');
CountProvider.of<CountModel>(context, listen: false).incrementCounter();
},
child: const Icon(Icons.add),
),
]);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _refresh,
tooltip: 'refresh',
child: const Icon(Icons.refresh),
),
);
}
}
每次点击 refresh,也会刷新 CountProvider 的依赖的子 widget ,这似乎并没有达到我们的要求。那该怎么解决呢?
3.3 优化(三)
3.3.1 InheritedBuildContext
抽象类, CountScopeElement 剥离出来需要用到功能和 value,方便后期扩展。
abstract class InheritedBuildContext<T> {
void needsBuild();
T get value;
}
3.3.2 Delegate
代理类 ,CountScopeElement 中方法的实现,value 的赋值。
abstract class Delegate<T> {
CountScopeElement<T> element;
T get value;
}
3.3.3 CountDelegate
Delegate 的实现类,主要实现了数据的监听,通知 InheritedElement 刷新依赖。
class CountDelegate<T extends ChangeNotifier> extends Delegate<T> {
CountDelegate({this.notifier});
T notifier;
@override
T get value {
notifier.addListener(() {
element.needsBuild();
});
return notifier;
}
}
3.3.4 CountScope
class CountScope<T> extends InheritedWidget {
const CountScope({@required this.delegate, Widget child}) : super(child: child);
final Delegate<T> delegate;
@override
bool updateShouldNotify(CountScope<T> oldWidget) {
return false;
}
@override
CountScopeElement<T> createElement() {
return CountScopeElement<T>(delegate, this);
}
}
class CountScopeElement<T> extends InheritedElement with InheritedBuildContext<T> {
CountScopeElement(this.delegate, InheritedWidget widget) : super(widget);
final Delegate<T> delegate;
bool _dirty = false;
@override
CountScope<T> get widget => super.widget as CountScope<T>;
@override
void performRebuild() {
delegate.element = this;
super.performRebuild();
}
@override
Widget build() {
if (_dirty) {
_dirty = false;
notifyClients(widget);
}
return super.build();
}
@override
void needsBuild() {
_dirty = true;
markNeedsBuild();
}
@override
T get value => delegate.value;
}
主要修改,当需要重新 build 时,_dirty 标记为 true,当 build 完成时,再设置为 false,避免当CountScope 刷新,导致依赖的子 widget 也全部刷新。
3.3.5 CountProvider
集成 SingleChildStatelessWidget。
class CountProvider<T extends ChangeNotifier> extends SingleChildStatelessWidget {
CountProvider({Key key, this.value, this.builder})
: _delegate = CountDelegate<T>(
notifier: value,
),
super(key: key);
final T value;
final WidgetBuilder builder;
final Delegate<T> _delegate;
static T of<T extends ChangeNotifier>(BuildContext context, {bool listen = true}) {
final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<CountScope<T>>();
final CountScopeElement<T> countScopeElement = inheritedElement as CountScopeElement<T>;
if (listen) {
context.dependOnInheritedElement(inheritedElement);
}
return countScopeElement.value;
}
@override
Widget buildWithChild(BuildContext context, Widget child) {
print('CountProvider buildWithChild');
return CountScope<T>(delegate: _delegate, child: Builder(builder: builder));
}
}
最终结果:当 CountScope 重新 rebuild 时,依赖的子 widget 不会跟着一起 rebuild。
3.3.5 小结
小结:这里优化三个方案,也只是带大家发现问题,一步一步去解决问题。其实,跟大家的业务结合的话,这上面的代码,或许还会有不满足务求的地方。但是,不管什么框架,也都是一步一步优化过来的。只要有了解决问题的思路和想法,我相信,任何问题都不在是问题。
四、 总结
前面分析 InheritedWidget 的源码,调用流程等,在实际开发过程中,我们可能不会直接接触 InheritedWidget ,但是,还是能发现一些系统的组件,使用 InheritedWidget 进行封装开发。例如:Theme、Focus 、ButtonBarTheme 等,就不一一举例了,大家可以去看下源码。写这篇文章,一方面是为了巩固知识,方便日后查看、回忆知识点。另一方便也是抛砖引玉,让更多人了解 InheritedWidget,如果大家认真看,其实还是会发现一些 provider 的影子。
如果文章有什么不足的地方,欢迎批评指正,谢谢~