150行代码写一个简单的Flutter状态管理组件
前情提要
大概是四月份左右,裸辞了一波。之后就一直在打游戏、复习、面试中循环度日,到现在还没有一个特别满意的结果。
感觉自己开始往佛系的方向发展了,难道这就是大起大落后的大彻大悟吗?
上面的话就权当开个玩笑,本篇文章的起因是在某次面试中,一位面试官问我Flutter里跨组件通信有哪些方式,我说的其中一种就是做一个统一管理,这样全局获取后就可以跨组件通信了,不过面试官没有给到一个正面的反馈,所以我就打算做一个这样的状态管理组件出来。如果下次再有人问我这个问题,我就会告诉他——“我给你讲讲我写的一个组件吧(微笑)”
下面开始正题
Flutter的刷新流程
想要做一个状态管理的组件,首先得了解一下Flutter的刷新流程,在前面写的 《从源码看Flutter系列》, 已经对这一过程有所了解,下面再简单介绍一下
- 调用
setState()
后,将对应的Element
添加到BuildOwner
维护的_dirtyElements
列表中 - 等待
engine
的frame
回调通知,会触发WidgetsBinding
的drawFrame()
方法,然后会遍历之前的_dirtyElements
,根据Element
在树中的高度,由上到下调用其rebuild()
方法进行重新创建或更新 Element
的刷新过程中,会将需要重新layout、paint的RenderObject
存放在PipelineOwner
维护的各个列表里,之后会在RendererBinding
的drawFrame()
方法里对RenderObject
来一个统一的更新- 刷新结束后,就是通过
BuildOwner
的finalizeTree()
来进行统一的销毁操作了
以上就是刷新流程的一个大致介绍。通过这个流程我们知道,对于需要更新或者销毁的对象,Flutter的做法就是放入一个列表中进行统一操作,在了解到这个事实后,显然组件状态也是可以统一管理的,这也就是后面将要实现的状态管理组件的核心原理啦。
InheritedElement与刷新
在正式介绍状态管理组件之前,我还是要先介绍一下 InheritedElement
这个常见嘉宾,Flutter中的全局主题修改等都是基于这个对象的,它对应的 Widget
是 InheritedWidget
,通过使用 InheritedWidget
,我们也可以做到跨组件通信。不过我个人总觉得它的使用方式不太美观,所以几乎很少用到。
现在非常受欢迎的 provider
库与之前的 scope_model
,都是基于 InheritedElement
来实现的,但是在使用 provider
的过程中会遇到这样一个问题:
当你在 PageC
通过 Provider.of<ModelB>(context)
来获取 PageB
对应的 Model
时,是会报错的,因为获取到的对象为null。
导致报错其实涉及到两个原因,分别与 InheritedElement
和页面栈相关,下面就来简单的说明一下。
InheritedElement的传递
provider中常用 Provider.of<T>(context)
来获取对应的数据对象,最终调用的都是 BuildContext
中的 getElementForInheritedWidgetOfExactType
方法,它的实现如下
///ElementMap<Type, InheritedElement> _inheritedWidgets;
///InheritedElement
@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
...
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
查找是通过而 _inheritedWidgets
来进行的,而它在 InheritedElement
中是如何传递的呢?
///InheritedElement@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;
}
就是通过 copy 父节点的 _inheritedWidgets
来达到传递效果,这在之前的《从源码看Element》中就已经提到过
到这里就知道了 InheritedElement
是如何传递和查找的了,接下来我们看一下导致 provider
无法获取对象的另外一个原因
页面栈的结构
我们打开和弹出一个页面,都是通过 Navigator
来操作的,而最终所有的页面都会被封装到 OverlayEntryWidget
中,被添加到 _Theatre
所持有的 children
列表里,也就是说所有的页面在数据结构上实际是平级的关系,下面用一个简单的图形表示一下
因为 InheritedElement
的查找就是通过父节点向上遍历,直到找到指定的对象为止,否则返回null,而这里由于 PageC 与 PageB 是平级的关系,显然 PageC 无法找到 PageB 对应的数据(实际上是对应的Element为平级,这里做了简化)
这也就是使用 provider
会遇到这样问题的原因,当然解决办法也很简单,就是将 Model
都放入 GlobalModel
中,通过 GlobalModel
获取即可
上面介绍完的这些对于理解状态管理有一定的帮助,下面就开始正式介绍我是如何实现状态管理组件的
实现状态管理组件
实现的思路非常简单,就是通过维护一个 HashMap
对象,将各个页面对应的 Model
放入其中,获取的时候通过这个 HashMap
获取即可。
不过可能会遇到下面这种场景:
当需要push多个相同的页面时,会有多个同类型的 Model
对象,显然这在 HashMap
中是无法通过类型来获取指定 Model
的,解决办法也很简单,那就是再维护一个 HashMap
,而 key
由使用者指定,这样就不必担心冲突的问题了
原理大致就是这样,最终代码如下
class ModelWidget<T extends Model> extends StatefulWidget {final ChildBuilder<T> childBuilder;
final ModelBuilder<T> modelBuilder;
final String modelKey;
const ModelWidget(
{Key key,
@required this.childBuilder,
@required this.modelBuilder,
this.modelKey})
: super(key: key);
@override
_ModelWidgetState createState() => _ModelWidgetState<T>();
}
typedef ChildBuilder<T extends Model> = Widget Function(
BuildContext context, T model);
typedef ModelBuilder<T extends Model> = T Function();
class _ModelWidgetState<T extends Model> extends State<ModelWidget<T>> {
...
}
class Model { ... }
class _StateDelegate { ... }
class ModelGroup {
static Map<Type, Model> _map = new HashMap();
static Map<String, Model> _repeatMap = new HashMap();
static void _pushModel(Model model) => _map[model.runtimeType] = model;
static void _pushModelWithKey(String key, Model model) =>
_repeatMap[key] = model;
static void _popModel(Model model) => _map.remove(model.runtimeType);
static void _popModelWithKey(String key, Model model) => _repeatMap.remove(key);
static T findModel<T extends Model>() => _map[T];
static T findModelByKey<T extends Model>(String key) => _repeatMap[key];
}
由于总共的代码量非常少,对细节有兴趣的小伙伴可以直接去看源码
使用方式如下
🔑 使用方式
首先定义你的 Model
对象
class YourModel extends Model {@override
void initState() {...}
@override
void dispose() {...}
int value = 0;
}
当你想要把它与某个Widget或页面结合使用时,可以像下面这样
ModelWidget<YourModel>(childBuilder: (ctx, model) => YourWidgetOrPage(),
modelBuilder: () => YourModel(),
),
🔄 获取数据与刷新
获取数据
final model = ModelGroup.findModel<YourModel>();
刷新
model.refresh();
你也可以直接尝试一下这个在线demo,点击体验
最后
裸辞期间总共开源了两个组件:
- 一个就是这个完成不久的 easy_model
- 另一个是markdown的渲染组件: markdown_widget
以上是 150行代码写一个简单的Flutter状态管理组件 的全部内容, 来源链接: utcz.com/a/18463.html