不到150行代码,写一个简单的Flutter状态管理组件

1,295 阅读6分钟

前情提要

大概是四月份左右,裸辞了一波。之后就一直在打游戏、复习、面试中循环度日,到现在还没有一个特别满意的结果。

感觉自己开始往佛系的方向发展了,难道这就是大起大落后的大彻大悟吗?

上面的话就权当开个玩笑,本篇文章的起因是在某次面试中,一位面试官问我Flutter里跨组件通信有哪些方式,我说的其中一种就是做一个统一管理,这样全局获取后就可以跨组件通信了,不过面试官没有给到一个正面的反馈,所以我就打算做一个这样的状态管理组件出来。如果下次再有人问我这个问题,我就会告诉他——“我给你讲讲我写的一个组件吧(微笑)”

下面开始正题

Flutter的刷新流程

想要做一个状态管理的组件,首先得了解一下Flutter的刷新流程,在前面写的 《从源码看Flutter系列》, 已经对这一过程有所了解,下面再简单介绍一下

  • 调用 setState() 后,将对应的 Element 添加到 BuildOwner 维护的 _dirtyElements 列表中
  • 等待 engineframe 回调通知,会触发 WidgetsBindingdrawFrame() 方法,然后会遍历之前的 _dirtyElements ,根据 Element 在树中的高度,由上到下调用其 rebuild() 方法进行重新创建或更新
  • Element 的刷新过程中,会将需要重新layout、paint的 RenderObject 存放在 PipelineOwner 维护的各个列表里,之后会在 RendererBindingdrawFrame() 方法里对 RenderObject 来一个统一的更新
  • 刷新结束后,就是通过 BuildOwnerfinalizeTree() 来进行统一的销毁操作了

以上就是刷新流程的一个大致介绍。通过这个流程我们知道,对于需要更新或者销毁的对象,Flutter的做法就是放入一个列表中进行统一操作,在了解到这个事实后,显然组件状态也是可以统一管理的,这也就是后面将要实现的状态管理组件的核心原理啦。

InheritedElement与刷新

在正式介绍状态管理组件之前,我还是要先介绍一下 InheritedElement 这个常见嘉宾,Flutter中的全局主题修改等都是基于这个对象的,它对应的 WidgetInheritedWidget,通过使用 InheritedWidget,我们也可以做到跨组件通信。不过我个人总觉得它的使用方式不太美观,所以几乎很少用到。

现在非常受欢迎的 provider 库与之前的 scope_model,都是基于 InheritedElement 来实现的,但是在使用 provider 的过程中会遇到这样一个问题:

当你在 PageC 通过 Provider.of<ModelB>(context) 来获取 PageB 对应的 Model 时,是会报错的,因为获取到的对象为null。

导致报错其实涉及到两个原因,分别与 InheritedElement 和页面栈相关,下面就来简单的说明一下。

InheritedElement的传递

provider中常用 Provider.of<T>(context) 来获取对应的数据对象,最终调用的都是 BuildContext 中的 getElementForInheritedWidgetOfExactType 方法,它的实现如下

  ///Element
  Map<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
    (主要是为了实现我用flutter写的个人Web博客)

同时,最后声明一下:落魄小哥,在线求职

有好的内推机会请务必不要放过我,我的联系方式就在上面的博客地址中

image