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

前情提要

大概是四月份左右,裸辞了一波。以后就一直在打游戏、复习、面试中循环度日,到如今尚未一个特别满意的结果。git

感受本身开始往佛系的方向发展了,难道这就是大起大落后的大彻大悟吗?github

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

下面开始正题bash

Flutter的刷新流程

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

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

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

InheritedElement与刷新

在正式介绍状态管理组件以前,我仍是要先介绍一下 InheritedElement 这个常见嘉宾,Flutter中的全局主题修改等都是基于这个对象的,它对应的 WidgetInheritedWidget,经过使用 InheritedWidget,咱们也能够作到跨组件通讯。不过我我的总以为它的使用方式不太美观,因此几乎不多用到。ide

如今很是受欢迎的 provider 库与以前的 scope_model,都是基于 InheritedElement 来实现的,可是在使用 provider 的过程当中会遇到这样一个问题:post

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

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

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
相关文章
相关标签/搜索