Flutter架构组件(FAC)——Flutter解耦架构模型和跨组件状态管理

FAC(Flutter Architecture Components)

FAC Flutter 架构组件(Flutter Architecture Components) 致力于解决Flutter跨组件刷新和集中化状态管理、关注点分离、构建高性能Flutter页面;

源码请查看:Github

Feature

与Provider相比:

  • FAC提供了强大的解耦装置ViewModel
  • 更为简洁的状态关联方式;
  • 以页面或APP纬度的集中化状态管理ViewModelStore
  • 更高效的刷新机制(支持LiveState对状态隔离,memo过滤参数列表)
  • 在initState中可以触发状态更新,比如加载数据,初始化页面数据等

FAC架构组件模型

image.png

没错,FAC看起来与AAC(Android Architecture Components)框架如出一辙,我们构建了带有生命周期监听的可观察数据对象LiveState,用于实现状态和UI控制器绑定;并实现ViewModelStoreProxyWidget用来存储和卸载ViewModel对象——当然在业务开发中,我们并不需要关注它的存在,会在架构层合适的位置进行注入;

这一切,对熟悉Android原生AAC组件的开发者来说,是一个福音;因为相对大而笨重的Provider,复杂的Redux框架来说,对FAC的学习是零成本!

使用场景

  • 组件状态共享,组件间状态可被观察
  • 跨组件通信/刷新
  • 任意代码位置获取目标组件状态并刷新
  • 某个组件需要改变全局状态
  • 构建大型复杂业务及多页面嵌套场景

安装 FAC

修改 pubspec.yaml

dependencies:
  pikach_fac: ^1.0.3

使用

三步搞定企业级复杂业务场景、代码分层和状态管理:

  • 1.定义ViewModel和Model数据类型
class PageViewModel extends ViewModel {
  LiveState<PageModel> pageModel = LiveState<PageModel>(PageModel());

  ///定义静态方法,便于任意位置获取PageViewModel的实例(可选)
  static PageViewModel get(BuildContext context) {
    return ViewModel.of<PageViewModel>(
      context,
      builder: () => PageViewModel(),
    );
  }
  ///定义业务的实现
  void onCountClick(BuildContext context) {
    PageViewModel.get(context).pageModel.setState((value, _) {
      _.state.count++;
    });
  }

}

  • 2.定义LiveState
LiveState<PageModel> pageModel = LiveState<PageModel>(PageModel());
  • 3.UI和状态绑定
  ///建立数据和UI的绑定关系
  PageViewModel.get(context).pageModel.buildWithObserve((ctx, _state) {

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Row(
        children: <Widget>[
          Text(
            '刷新优化,只有count变化才刷新: ${_state.count}',
          ),
        ],
      ),
    );
  }, memo: (value) => [value.count])

详细代码请查阅Github

数据流

image.png

架构设计背景

架构设计的初衷,是为了解决Flutter在复杂的嵌套页面场景下,能够有效的管理页面的状态和刷新效率;对于简单的业务场景,同样有友好的代码分层和状态管理能力!

以下是业务中采用FAC开发的Flutter页面:


image.png

组件

1. LiveState组件

LiveState:可被观察的数据组件;称之为Live的原理,是因为它可以感知Widget组件的生命周期!

LiveState 可用于构建可被观察的数据对象,即构建model;在数据发生变更时,并且在生命周期合法的情况,能够及时通知到观察者;用于分发或转发数据对象(MediatorLiveState可以用作转发或合并数据源);

LiveState可以单独使用,去构建跨组件刷新模型;
但是我们更推荐将其放在 ViewModel 组件中,这样可以将model对象转化成 ViewModel ,从而构建 UI 和 Model 分离的代码结构!

//在ViewModel中实例化LiveState对象
LiveState<ReportRequestModel> _reportRequest;

LiveState<ReportRequestModel> get reportRequest =>
  _reportRequest ??= LiveState<ReportRequestModel>(ReportRequestModel());

其他接口

LiveState提供其它相关接口,用于响应式编程开发;

  • StateOwner 用于提供state的整体变更
class StateOwner<T> {
  T state;
} 
  • MediatorLiveState 用于合并多个LiveState数据源,更利于构建响应式应用
    image.png
  • Transformations 用于LiveState数据源变化,由于dart不支持反射,可能并没有那么好用,建议直接用 MediatorLiveState 替代

2. ViewModel组件

ViewModel:分离数据和 UI,专注于逻辑处理,构建数据和UI的双向绑定;

另外,该类中提供appContext用于navigation等与element无关的调用,该context来自于VMS!

1.生命周期安全

ViewModel 的声明周期与当前页面周期是一致的,不需要担心内存泄漏或关注回收的工作;

2.数据和UI解耦

直接使用 Provider,需要手动构建 ViewModel 构建数据和 UI 的解耦模型;而 ViewModel 组件为解决该问题应运而生!
ViewModel 可以用来管理UI所需的相关数据,承担UI和Model之间的交互和处理业务逻辑。
在页面的任何位置,我们可以通过ViewModel.of<T>(context)获取相应的 ViewModel 实例,从而建立UI和Model的双向关联
ViewModel 可以胜任各种复杂的交互、业务逻辑、异步调用等;我们所有的修改都基于model,model 会自动关联注册的UI对象(Flutter Widget),从而构建一个单一数据源的模型;
因为组件的刷新是通过Model自动关联的(而不是调用 State 中的 setState()方法),我们更期望通过仅使用StatelessWidget去构建应用!

3.构建跨组件通信接口

ViewModel用来提供自定义组件刷新接口
比如一个 AppBar需要提供title改变的接口,或 AppBar 的右侧Action中提供一个购物车数量指示。典型的跨组件刷新问题,可以通过 ViewModel 向往提供接口,开发者可以按需调用!

3. ViewModelStoreProxyWidget 组件

类似Redux中Store但是 ViewModelStoreProxyWidget 中共享的数据是当前页面相关的;开发者往往不需要关注它的存在,因为我们会通过框架在合适的位置注入。

_ViewModelStore 数据存储组件

类似Android中 lifecycle-viewmodel 组件包里的 ViewModelStore 组件,ViewModelStoreProxyWidget 作为页面顶级Widget,通过 InheritedWidget 共享页面的数据!当然,它的存储方式和原生端 VMS 如出一辙,采用Map<Type, dynamic>()对ViewModel进行存储;另外,我们为了适应复杂的大型应用场景,增加了final _factoriesByName = Map<String, dynamic>();的存储方式,可以存储多个同名的 ViewModel,使得单页面中的同组件类型接口问题得以解决!

设计原理

image.png

代理拦截与代理穿透

  • 代理拦截

我们在任何位置可以轻松获取数据源并刷新UI的便利很大程度来源于代理拦截机制。如下,ViewModel.of<CountViewModel>(context).model

所谓的代理机制,原理很简单,我们会通过Provider在合适位置注册VMS进行节点拦截,因为inheritWidget会在最近位置通过泛型获取数据,从而达到拦截的目的。
收益:

  • 防止外层页面和当前子页面数据混用
  • 在任何位置可以获取数据并刷新UI
  • 代理穿透

供代理拦截机制后,我们如何获取外层组件数据呢?
ViewModel提供了一种代理穿透的方式获取更外层数据。

代理拦截和代理穿透模式如下


image.png
代理截断的场景.png

代理穿透演示


image.png

共享Stroe

页面通过 instanceName 参数,可以在一个VMS下面共享多个同类型ViewModel;从而在一个页面下面,可以嵌套多个同类型组件,并调用不同组件的方法;

拓展

自定义组件接口

自定义组件如何对外提供组件刷新接口?

直接使用Provider

Provider的方式是通过在上层节点上挂载Provider<T>通过ChangeNotifier构建的Model刷新 Provider 实例刷新Consumer<T>下的子组件。这个过程对于我而言,异常的漫长,且过于耦合;不能提供一个对外跨组件刷新的接口。

使用FAC

有没有什么方式能通过,提供自定义组件的同时,提供组件的对外刷新接口,且这个组件刷新接口可以在代码的任何位置被调用?比如 AppBar 提供标题修改的接口,购物车提供商品数量变化的接口;

  • LoadingPage实现
    LoadingPage是页面状态管理组件,有页面加载、错误、空数据、显示等状态;

由于使用FAC时,可以在页面任意位置获取ViewModel的实例,我们可以在开发自定义Widget LoadingPage的时候提供一个VM接口;外部可以使用该接口进行刷新:

PkcPageViewModel.get(context, instance: instance).pageLiveData.setState((value, _) {
        value.pageState = PkcPageState.state_loading;
      });

请查阅关于LoadingPage的组件实现Github

  • AppBar刷新和购物车接口


    image.png

    image.png

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342