讲在前面
对于刚上手开发Flutter的同学,想要实现一个Widget的刷新,除了使用StatefulWidget+setState方法,似乎没有什么更好的方式;
更深入一点之后发现,可以使用一些状态管理库来实现Widget的刷新,似乎更加方便而且规范了;
比如官方提供的Provider状态管理库,我们可以使用其提供的ChangeNotifierProvider
来实现刷新,在我们的状态类(继承ChangeNotifier
)中调用notifyListeners
之后,我们在ChangeNotifierProvider
child内有声明context.watch()或使用Consumer/Selector等包裹的地方就会进行刷新;
比较神奇的地方在于,在一个复杂的Widget树中,这些库可以帮助我实现某些Widget的局部刷新,避免一些高频次的整体重建(尽管framework源码中对于Element树的构建有足够多的逻辑优化,我们还是需要尽量避免无意义的Widget刷新)
为了搞清楚这个机制的实现原理,这里我们自制一个简易版Provider作为切入口来分析:
先来个demo图示:
然后看看代码内容
示例代码
视图 & 状态
class SamplePage extends StatelessWidget {
const SamplePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("SamplePage"),
),
body: MyChangeNotifierProvider<SampleModel>(_buildBody(), SampleModel()),
);
}
_buildBody() {
return SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Builder(builder: (context) {
return Text(
"CountA is ${(MyInheritedProvider.of(context, listen: true).model as SampleModel).count.toString()}");
}),
Builder(builder: (context) {
return Text(
"CountB is ${(MyInheritedProvider.of(context, listen: false).model as SampleModel).count.toString()}");
}),
Builder(builder: (context) {
return GestureDetector(
onTap: () {
(MyInheritedProvider.of(context, listen: false).model
as SampleModel)
.countIncrease();
},
child: const Text("launch"),
);
})
],
),
);
}
}
class SampleModel extends MyChangeNotifier {
int count = 0;
countIncrease() {
count++;
notifyListener();
}
}
自制Provider相关
class MyChangeNotifier {
Function? notifyFunc;
registerListener(Function func){
notifyFunc = func;
}
notifyListener() {
notifyFunc?.call();
}
}
class MyInheritedProvider<T extends MyChangeNotifier> extends InheritedWidget {
final T model;
const MyInheritedProvider(
this.model, {
Key? key,
required Widget child,
}) : super(key: key, child: child);
static MyInheritedProvider of(BuildContext context, {bool listen = false}) {
if (listen) {
final MyInheritedProvider? result =
context.dependOnInheritedWidgetOfExactType<MyInheritedProvider>();
assert(result != null, 'No MyInheritedProvider found in context');
return result!;
} else {
final MyInheritedProvider? result = context
.getElementForInheritedWidgetOfExactType<MyInheritedProvider>()!
.widget as MyInheritedProvider?;
assert(result != null, 'No MyInheritedProvider found in context');
return result!;
}
}
@override
bool updateShouldNotify(MyInheritedProvider old) {
return true;
}
}
class MyChangeNotifierProvider<T extends MyChangeNotifier>
extends StatefulWidget {
final Widget child;
final T model;
const MyChangeNotifierProvider(this.child, this.model, {Key? key})
: super(key: key);
@override
State<MyChangeNotifierProvider> createState() =>
_MyChangeNotifierProviderState();
}
class _MyChangeNotifierProviderState extends State<MyChangeNotifierProvider> {
doSetState() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return MyInheritedProvider(
widget.model,
child: widget.child,
);
}
@override
void didUpdateWidget(
covariant MyChangeNotifierProvider<MyChangeNotifier> oldWidget) {
super.didUpdateWidget(oldWidget);
widget.model.registerListener(doSetState);
}
}
上面这一大坨代码,就是我们Demo的全部代码,分为两部分
- 我们自己的业务代码(第一部分)
- 我们自制的ChangeNotifierProvider(第二部分)
这里我们为了更好的与官方的Provider库对应上,所以自制的Provider框架类前面都加了个my,方便理解;
这里我们说明下具体类的功能
类名 | 功能 |
---|---|
sample_page | 页面类,其中展示了3个Text,前两个引用了SampleModel中的count值,但是A进行了监听,B没有 |
sample_model | 状态类,继承了MyChangeNotifier,其中只有一个count变量和改变count的简单方法 |
my_change_notifier | 仅仅存储一个Function,合适的时机调用该Function |
my_inherited_provider | 核心类,继承于InheritedWidget,其中存放一个数据模型,泛型限制继承于MyChangeNotifier。提供了一个获取当前类的方法,入参区分是否进行监听(关联依赖) |
my_change_notifier_provider | 核心类,继承于StatefulWidget,接收一个Widget和一个数据模型,最终在State的build方法中均传入MyInheritedProvider |
简单介绍完之后,我们来具体分析一下:
SamplePage
首先是页面(SamplePage
),比较简单,写法跟ChangeNotifierProvider
一样,在页面之上包裹一个我们的MyChangeNotifierProvider
,里面是一个列表,里面有三个Text
,前两个是展示count
数值,后一个是点击后去增长count
值的;
注意这里我们的几个Widget
都是用Builder
包裹了一层,这里提前说明一下是为了使用其BuildContext
去获取组件树上的MyInheritedProvider
。
MyChangeNotifierProvider
然后我们来看一看MyChangeNotifierProvider
类:
我们直接看其State
类,很简单:
在didUpdateWidget
钩子函数中将setState
方法注册进入我们的MyChangeNotifier
中;
didUpdateWidget
方法在以下情况下会被调用:
- 当与该
State
对象关联的Widget
重新构建并创建一个新的Widget
实例时。- 当父
Widget
改变并重新构建该StatefulWidget
时,Flutter 框架会调用didUpdateWidget
方法。
build
方法将外部传入的model
和child
都传入MyInheritedProvider
中;
总的来说这个类的主要工作就是注册一下刷新方法,供状态类在某个时机下调用;并且在build
时对于传入的Widget
包裹了一层MyInheritedProvider
返回。看起来这就是一个简单的中介类。
MyInheritedProvider
进入MyInheritedProvider
,这是最核心的部分,此类继承于InheritedWidge
;
InheritedWidget
最重要的功能之一在于,可以通过BuildContext
的dependOnInheritedWidgetOfExactType
或getElementForInheritedWidgetOfExactType
方法获取祖先组件树中的InheritedWidget
类型的Widget
,并且前者的方法中有一个依赖注册的功能,这点我们会分析到;
另外一点也很重要,InheritedWidget
对应的Element
是InheritedElement
,其父类是ProxyElement
,它是继承与ComponentElement
的,不过它的build
方法并不像StatelessElement
一样是调用自己Widget
的build
方法,而是直接返回了Widget
的child
变量
好了,分析完这两个核心类,我们来看看代码运行后的表现以及为何如此。
回到我们的SamplePage
,我们点击了"launch"按钮,此时调用了SampleModel
中的countIncrease
方法:将count++
,并且调用notifyListener
方法;
这个方法对应的就是_MyChangeNotifierProviderState
中的setState
方法,这时候build
方法执行,重新返回了一个MyInheritedWidget
;
可能刚接触Flutter的同学就出现疑惑了,理论上来说StatefulWidget
调用了setState
方法之后,其子类会进行刷新,而我们传入的三个Text
都是StatefulWidget
的子类(StatefulWidget
- MyInheritedWidget
- 3个Text
),为什么会只刷新了其中的一个Text呢?
源码分析
我们开始追踪源码;
我们要知道一个前提:刷新Widget会先进入Element
的rebuild
方法。然后是performRebuild
方法,这个方法Element
没做什么,交由具体子类去实现。StatefulWidget
的Element
的是ComponentElement
,所以我们来看看它的具体实现:
这里的build
方法,即我们的MyInheritedProvider
:
注意这里的build
方法,是ComponentElement
独有方法,这里返回的MyInheritedProvider
即updateChild
方法传入的built
参数,这里解释下这3个入参:
变量 | 类型 | 含义 |
---|---|---|
_child | Element | 当前Element持有的子Elmenet,第一次执行时或上一次没有child时为null |
built | Widget | 即调用自身build返回的Widget对象,build方法具体实现交由子类(比如我们常写的StatelessWidget中的build方法) |
slot | Object | slot 是一个用于标识元素在其父元素中的位置或角色的抽象概念。它通常用于复杂的布局逻辑,其中子元素之间的关系并不仅仅是一个简单的线性列表,这个点本文不做具体解释,此部分不影响本文分析内容 |
接着看updateChild
方法,我们先总结一下这个方法的工作:就是传入build
返回的Widget
和之前加载Element
树时已生成的子Element
做各种比较,判断要不要重新通过Widget
生成一个新的Element
,还是说仍然使用之前的Element
子类,只是做一下更新Widget
动作;
这里我们把不重要的代码先删除掉,图示分析一下这个方法中都做了什么:
回到我们的代码中,我们点击了"launch"按钮,执行了setState
,然后进入performRebuild
,又进入updateChild
方法,这里child
是第一次运行时就生成的InheritedElement
,newWidget
是传入的MyInheritedProvider
;
- 条件1,判断不进入,因为
build
方法返回的是一个新的MyInheritedProvider
,跟之前Element
持有的并不是同一个对象 - 条件2,判断进入,因为运行时类型是一样的(并且我们没有给
Widget
传入key参数)
那么这里就执行了child.update(newWidget)
update
在Element
类中只做了一个重新赋值_widget
的操作:
我们还是要看具体子类有没有重写该方法,InheritedElement->ProxyElement->ComponentElement->Element
三个子类中只有ProxyElement
进行了重写:
这里1稍微放一放,我们看看2的逻辑;
这里我们要记住我们当前执行的已经是在InheritedElement
对象中的方法了,因为它跟StatefulElement
一样也是ComponentElement
的子类,最终也会走到上面的performRebuild
方法,然后调用自己的build()
方法,返回一个built
传入updateChild
方法;
好了,这里就是我们要重点分析的地方了,为什么没有刷新我们的业务Widget(这里就暂且称我们SamplePage中传入MyChangeNotifierProvider
的child
为业务Widget,即下图_buildBody
中Widget
)
一、我们setState
刷新的是State
类,而State#build
方法中返回的MyInheritedProvider
中的child
不是重新创建的,而是一开始外部传入StatefulWidget
中存储的;
二、记得上面StatefulElement
这一层执行到了child.update(newWidget)
,进入了InheritedElement
这一层,它的update
方法中仅替换了Element
持有的Widget
对象(Element
没有重新创建),然后进入了ComponentElement#performRebuild
方法,这里执行了自己的build
方法去获取一个Widget
,这个Widget
是什么呢?回顾一下
它就是我们的业务Widget,一直作为child
变量存储在ProxyWidget
中,这里的ProxyElement#build
方法只是将其拿了出来,并没有重新创建一个Widget
;
三、那么看到我们的updateChild
方法中(上翻一下updateChild
方法图示),自然就进入了条件分支1中,因为等式两边都是我们的业务Widget(同一个对象)
所以,组件树从上向下更新的过程中到了这里就中断了,不会向下再进行了;
现在我们要来研究一下最后的问题:为什么组件Widget
中的其中一个Text
可以被刷新?
我们来看下两个Text
分别怎么展示自己的text内容的:
这个参数区分是使用了什么方法来获取组件树中的MyInheritedProvider
CountA使用的方法:BuildContext# dependOnInheritedWidgetOfExactType
CountB使用的方法:BuildContext# getElementForInheritedWidgetOfExactType
这里直接说明一下区别,前者方法比后者多一个功能:
先在组件树祖先中找到指定类型的InheritedElement
,然后将当前的Element
依赖到对应的InheritedElement
中,使用一个Map容器(_dependents
)来存储;
那么这些容器里的Element
又是在哪里被拿出来使用的呢?是怎么使用的呢?
还记得之前讲的setState
之后,执行到了MyInheritedProvider
对应Element
的update
方法吗(拿出来再看一眼)
我们进去看一看都有什么动作
进入了ProxyElement
的updated
方法,不过它的子类InheritedElement
重写了这个方法,看一眼
还记得这个方法吗,我们的MyInheritedProvider
继承于InheritedWidget
,必须要重写这个方法,来决定是否应该通知依赖,我们为了简单直接return了true(可以根据具体业务决定是否通知),所以逻辑执行了super.updated
。接着向里看
终于,看到了熟悉的方法markNeedsBuild
,把当前Element
标记为需要更新,后续则通过BuildOwner
展开了组件的刷新逻辑(这部分等同于StatefulWidget
的State
中调用了setState
,不做展开了)
说在最后
总结一下:
到此为止,我们终于搞定了一个丐版的自制可局部刷新的状态管理框架~🎉🎉,至于官方ChangeNotifierProvider
的实现逻辑,其实实现逻辑不尽相同,我们后续再专门做一篇分析;
这个整体的实现核心逻辑就是Flutter框架中提供的InheritedWidget
组件,这个组件的重要性不亚于我们最常使用的StatelessWidget
、StatefulWidget
,了解了其核心逻辑,我们也可以使用它来写出一些优雅的框架等;
最后贴一下上述的demo,里面添加了部分注释,大家可以clone下来debug一下增加理解。
以上如有错误,欢迎指出!