Flutter完整开发实战详解(12、全面深刻理解状态管理设计)

做为系列文章的第十二篇,本篇将经过 scope_model 、 BloC 设计模式、flutter_redux 、 fish_redux 来全面深刻分析, Flutter 中你们最为关心的状态管理机制,理解各大框架中如何设计实现状态管理,从而选出你最为合适的 state “大管家”。前端

前文:git

在全部 响应式编程 中,状态管理一直老生常谈的话题,而在 Flutter 中,目前主流的有 scope_modelBloC 设计模式flutter_reduxfish_redux 等四种设计,它们的 复杂度上手难度 是逐步递增的,但同时 可拓展性解耦度复用能力 也逐步提高。github

基于前篇,咱们对 Stream 已经有了全面深刻的理解,后面能够发现这四大框架或多或少都有 Stream 的应用,不过仍是那句老话,合适才是最重要,不要为了设计而设计编程

本文Demo源码redux

GSYGithubFlutter 完整开源项目设计模式

1、scoped_model

scoped_model 是 Flutter 最为简单的状态管理框架,它充分利用了 Flutter 中的一些特性,只有一个 dart 文件的它,极简的实现了通常场景下的状态管理。数组

以下方代码所示,利用 scoped_model 实现状态管理只须要三步 :bash

  • 定义 Model 的实现,如 CountModel ,而且在状态改变时执行 notifyListeners() 方法。
  • 使用 ScopedModel Widget 加载 Model
  • 使用 ScopedModelDescendant 或者 ScopedModel.of<CountModel>(context) 加载 Model 内状态数据。

是否是很简单?那仅仅一个 dart 文件,如何实现这样的效果的呢?后面咱们立刻开始剥析它。app

class ScopedPage extends StatelessWidget {
  final CountModel _model = new CountModel();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("scoped"),
        ),
        body: Container(
          child: new ScopedModel<CountModel>(
            model: _model,
            child: CountWidget(),
          ),
        ));
  }
}

class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
        return new Column(
          children: <Widget>[
            new Expanded(child: new Center(child: new Text(model.count.toString()))),
            new Center(
              child: new FlatButton(
                  onPressed: () {
                    model.add();
                  },
                  color: Colors.blue,
                  child: new Text("+")),
            ),
          ],
        );
      });
  }
}

class CountModel extends Model {
  static CountModel of(BuildContext context) =>
      ScopedModel.of<CountModel>(context);
  
  int _count = 0;
  
  int get count => _count;
  
  void add() {
    _count++;
    notifyListeners();
  }
}
复制代码

以下图所示,在 scoped_model 的整个实现流程中,ScopedModel 这个 Widget 很巧妙的借助了 AnimatedBuildler框架

由于 AnimatedBuildler 继承了 AnimatedWidget ,在 AnimatedWidget 的生命周期中会对 Listenable 接口添加监听,而 Model 刚好就实现了 Listenable 接口,整个流程总结起来就是:

  • Model 实现了 Listenable 接口,内部维护一个 Set<VoidCallback> _listeners
  • Model 设置给 AnimatedBuildler 时, ListenableaddListener 会被调用,而后添加一个 _handleChange 监听到 _listeners 这个 Set 中。
  • Model 调用 notifyListeners 时,会经过异步方法 scheduleMicrotask 去从头至尾执行一遍 _listeners 中的 _handleChange
  • _handleChange 监听被调用,执行了 setState({})

image.png

整个流程是否是很巧妙,机制的利用了 AnimatedWidgetListenable 在 Flutter 中的特性组合,至于 ScopedModelDescendant 就只是为了跨 Widget 共享 Model 而作的一层封装,主要仍是经过 ScopedModel.of<CountModel>(context) 获取到对应 Model 对象,这这个实现上,scoped_model 依旧利用了 Flutter 的特性控件 InheritedWidget 实现。

InheritedWidget

scoped_model 中咱们能够经过 ScopedModel.of<CountModel>(context) 获取咱们的 Model ,其中最主要是由于其内部的 build 的时候,包裹了一个 _InheritedModel 控件,而它继承了 InheritedWidget

为何咱们能够经过 context 去获取到共享的 Model 对象呢?

首先咱们知道 context 只是接口,而在 Flutter 中 context 的实现是 Element ,在 ElementinheritFromWidgetOfExactType 方法实现里,有一个 Map<Type, InheritedElement> _inheritedWidgets 的对象。

_inheritedWidgets 通常状况下是空的,只有当父控件是 InheritedWidget 或者自己是 InheritedWidgets 时才会有被初始化,而当父控件是 InheritedWidget 时,这个 Map 会被一级一级往下传递与合并

因此当咱们经过 context 调用 inheritFromWidgetOfExactType 时,就能够往上查找到父控件的 Widget,从在 scoped_model 获取到 _InheritedModel 中的Model

2、BloC

BloC 全称 Business Logic Component ,它属于一种设计模式,在 Flutter 中它主要是经过 StreamSteamBuilder 来实现设计的,因此 BloC 实现起来也相对简单,关于 StreamSteamBuilder 的实现原理能够查看前篇,这里主要展现如何完成一个简单的 BloC

以下代码所示,整个流程总结为:

  • 定义一个 PageBloc 对象,利用 StreamController 建立 SinkStream
  • PageBloc 对外暴露 Stream 用来与 StreamBuilder 结合;暴露 add 方法提供外部调用,内部经过 Sink 更新 Stream
  • 利用 StreamBuilder 加载监听 Stream 数据流,经过 snapShot 中的 data 更新控件。

固然,若是和 rxdart 结合能够简化 StreamController 的一些操做,同时若是你须要利用 BloC 模式实现状态共享,那么本身也能够封装多一层 InheritedWidgets 的嵌套,若是对于这一块有疑惑的话,推荐能够去看看上一篇的 Stream 解析。

class _BlocPageState extends State<BlocPage> {
  final PageBloc _pageBloc = new PageBloc();
  @override
  void dispose() {
    _pageBloc.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: new StreamBuilder(
            initialData: 0,
            stream: _pageBloc.stream,
            builder: (context, snapShot) {
              return new Column(
                children: <Widget>[
                  new Expanded(
                      child: new Center(
                          child: new Text(snapShot.data.toString()))),
                  new Center(
                    child: new FlatButton(
                        onPressed: () {
                          _pageBloc.add();
                        },
                        color: Colors.blue,
                        child: new Text("+")),
                  ),
                  new SizedBox(
                    height: 100,
                  )
                ],
              );
            }),
      ),
    );
  }
}
class PageBloc {
  int _count = 0;
  ///StreamController
  StreamController<int> _countController = StreamController<int>();
  ///对外提供入口
  StreamSink<int> get _countSink => _countController.sink;
  ///提供 stream StreamBuilder 订阅
  Stream<int> get stream => _countController.stream;
  void dispose() {
    _countController.close();
  }
  void add() {
    _count++;
    _countSink.add(_count);
  }
}
复制代码

3、flutter_redux

相信若是是前端开发者,对于 redux 模式并不会陌生,而 flutter_redux 能够看作是利用了 Stream 特性的 scope_model 升级版,经过 redux 设计模式来完成解耦和拓展。

固然,更多的功能和更好的拓展性,也形成了代码的复杂度和上手难度 ,由于 flutter_redux 的代码使用篇幅问题,这里就不展现全部代码了,须要看使用代码的可直接从 demo 获取,如今咱们直接看 flutter_redux 是如何实现状态管理的吧。

如上图,咱们知道 redux 中通常有 StoreActionReducer 三个主要对象,以外还有 Middleware 中间件用于拦截,因此以下代码所示:

  • 建立 Store 用于管理状态 。
  • Store 增长 appReducer 合集方法,增长须要拦截的 middleware,并初始化状态。
  • Store 设置给 StoreProvider 这个 InheritedWidget
  • 经过 StoreConnector / StoreBuilder 加载显示 Store 中的数据。

以后咱们能够 dispatch 一个 Action ,在通过 middleware 以后,触发对应的 Reducer 返回数据,而事实上这里核心的内容实现,仍是 StreamStreamBuilder 的结合使用 ,接下来就让咱们看看这个流程是如何联动起来的吧。

class _ReduxPageState extends State<ReduxPage> {

  ///初始化store
  final store = new Store<CountState>(
    /// reducer 合集方法
    appReducer,
    ///中间键
    middleware: middleware,
    ///初始化状态
    initialState: new CountState(count: 0),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: new Text("redux"),
        ),
        body: Container(
          /// StoreProvider InheritedWidget
          /// 加载 store 共享
          child: new StoreProvider(
            store: store,
            child: CountWidget(),
          ),
        ));
  }
}
复制代码

以下图所示,是 flutter_redux 从入口到更新的完整流程图,整理这个流程其中最关键有几个点是:

  • StoreProviderInheritedWidgets ,因此它能够经过 context 实现状态共享。
  • StreamBuilder / StoreConnector 的内部实现主要是 StreamBuilder
  • Store 内部是经过 StreamController.broadcast 建立的 Stream ,而后在 StoreConnector 中经过 Streammaptransform 实现小状态的变换,最后更新到 StreamBuilder

那么如今看下图流程有点晕?下面咱们直接分析图中流程。

能够看出整个流程的核心仍是 Stream ,基于这几个关键点,咱们把上图的流程整理为:

  • 一、 Store 建立时传入 reducer 对象和 middleware 数组,同时经过 StreamController.broadcast 建立了 _changeController 对象。
  • 二、 Store 利用 middleware_changeController 组成了一个 NextDispatcher 方法数组 ,并把 _changeController 所在的 NextDispatcher 方法放置在数组最后位置。
  • 三、 StoreConnector 内经过 Store_changeController 获取 Stream ,并进行了一系列变换后,最终 Stream 设置给了 StreamBuilder
  • 四、当咱们调用 Stroedispatch 方法时,咱们会先进过 NextDispatcher 数组中的一系列 middleware 拦截器,最终调用到队末的 _changeController 所在的 NextDispatcher
  • 五、最后一个 NextDispatcher 执行时会先执行 reducer 方法获取新的 state ,而后经过 _changeController.add 将状态加载到 Stream 流程中,触发 StoreConnectorStreamBuilder 更新数据。

若是对于 Stream 流程不熟悉的还请看上篇。

如今再对照流程图会不会清晰不少了?

flutter_redux 中,开发者的每一个操做都只是一个 Action ,而这个行为所触发的逻辑彻底由 middlewarereducer 决定,这样的设计在必定程度上将业务与UI隔离,同时也统一了状态的管理。

好比你一个点击行为只是发出一个 RefrshAction ,可是经过 middleware 拦截以后,在其中异步处理完几个数据接口,而后从新 dispatchAction1Action2Action3 去更新其余页面, 相似的 redux_epics 库就是这样实现异步的 middleware 逻辑。

4、fish_redux

若是说 flutter_redux 属于相对复杂的状态管理设置的话,那么闲鱼开源的 fish_redux 可谓 “不走寻常路” 了,虽然是基于 redux 原有的设计理念,同时也有使用到 Stream ,可是相比较起来整个设计彻底是 超脱三界,若是是前面的都是简单的拼积木,那是 fish_redux 就是积木界的乐高。

由于篇幅缘由,这里也只展现部分代码,其中 reducer 仍是咱们熟悉的存在,而闲鱼在这 redux 的基础上提出了 Comoponent 的概念,这个概念下 fish_redux 是从 ContextWidget 等地方就开始全面“入侵”你的代码,从而带来“超级赛亚人”版的 redux

以下代码所示,默认状况咱们须要:

  • 继承 Page 实现咱们的页面。
  • 定义好咱们的 State 状态。
  • 定义 effectmiddlewarereducer 用于实现反作用、中间件、结果返回处理。
  • 定义 view 用于绘制页面。
  • 定义 dependencies 用户装配控件,这里最骚气的莫过于重载了 + 操做符,而后利用 ConnectorState 挑选出数据,而后经过 Component 绘制。

如今看起来使用流程是否是变得复杂了?

可是这带来的好处就是 复用的颗粒度更细了,装配和功能更加的清晰。 那这个过程是如何实现的呢?后面咱们将分析这个复杂的流程。

class FishPage extends Page<CountState, Map<String, dynamic>> {
  FishPage()
      : super(
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          ///配置 View 显示
          view: buildView,
          ///配置 Dependencies 显示
          dependencies: Dependencies<CountState>(
              slots: <String, Dependent<CountState>>{
                ///经过 Connector() 从 大 state 转化处小 state
                ///而后将数据渲染到 Component
                'count-double': DoubleCountConnector() + DoubleCountComponent()
              }
          ),
          middleware: <Middleware<CountState>>[
            ///中间键打印log
            logMiddleware(tag: 'FishPage'),
          ]
  );
}

///渲染主页
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
      appBar: AppBar(
        title: new Text("fish"),
      ),
      body: new Column(
        children: <Widget>[
          ///viewService 渲染 dependencies
          viewService.buildComponent('count-double'),
          new Expanded(child: new Center(child: new Text(state.count.toString()))),
          new Center(
            child: new FlatButton(
                onPressed: () {
                  ///+
                  dispatch(CountActionCreator.onAddAction());
                },
                color: Colors.blue,
                child: new Text("+")),
          ),
          new SizedBox(
            height: 100,
          )
        ],
      ));
}
复制代码

以下大图所示,整个联动的流程比 flutter_redux 复杂了更多( 若是看不清能够点击大图 ),而这个过程咱们总结起来就是:

  • 一、Page 的构建须要 StateEffectReducerviewdependenciesmiddleware 等参数。

  • 二、Page 的内部 PageProvider 是一个 InheritedWidget 用户状态共享。

  • 三、Page 内部会经过 createMixedStore 建立 Store 对象。

  • 四、Store 对象对外提供的 subscribe 方法,在订阅时会将订阅的方法添加到内部 List<_VoidCallback> _listeners

  • 五、Store 对象内部的 StreamController.broadcast 建立出了 _notifyController 对象用于广播更新。

  • 六、Store 对象内部的 subscribe 方法,会在 ComponentState 中添加订阅方法 onNotify若是调用在 onNotify 中最终会执行 setState更新UI。

  • 七、Store 对象对外提供的 dispatch 方法,执行时内部会执行 4 中的 List<_VoidCallback> _listeners,触发 onNotify

  • 八、Page 内部会经过 Logic 建立 Dispatch ,执行时经历 Effect -> Middleware -> Stroe.dispatch -> Reducer -> State -> _notifyController -> _notifyController.add(state) 等流程。

  • 九、以上流程最终就是 Dispatch 触发 Store 内部 _notifyController , 最终会触发 ComponentState 中的 onNotify 中的setState更新UI

是否是有不少对象很陌生?

确实 fish_redux 的总体流程更加复杂,内部的 ContxtSysComponetViewSerivceLogic 等等概念设计,这里由于篇幅有限就不详细拆分展现了,但从整个流程能够看出 fish_redux控件到页面更新,全都进行了新的独立设计,而这里面最有意思的,莫不过 dependencies

以下图所示,得益于fish_redux 内部 ConnOpMixin 中对操做符的重载,咱们能够经过 DoubleCountConnector() + DoubleCountComponent() 来实现Dependent 的组装。

Dependent 的组装中 Connector 会从总 State 中读取须要的小 State 用于 Component 的绘制,这样很好的达到了 模块解耦与复用 的效果。

而使用中咱们组装的 dependencies 最后都会经过 ViewService 提供调用调用能力,好比调用 buildAdapter 用于列表能力,调用 buildComponent 提供独立控件能力等。

能够看出 flutter_redux 的内部实现复杂度是比较高的,在提供组装、复用、解耦的同时,也对项目进行了必定程度的入侵,这里的篇幅可能不能很全面的分析 flutter_redux 中的整个流程,可是也能让你理解整个流程的关键点,细细品味设计之美。

自此,第十二篇终于结束了!(///▽///)

资源推荐

完整开源项目推荐:
文章

《Flutter完整开发实战详解(1、Dart语言和Flutter基础)》

《Flutter完整开发实战详解(2、 快速开发实战篇)》

《Flutter完整开发实战详解(3、 打包与填坑篇)》

《Flutter完整开发实战详解(4、Redux、主题、国际化)》

《Flutter完整开发实战详解(5、 深刻探索)》

《Flutter完整开发实战详解(6、 深刻Widget原理)》

《Flutter完整开发实战详解(7、 深刻布局原理)》

《Flutter完整开发实战详解(8、 实用技巧与填坑)》

《Flutter完整开发实战详解(9、 深刻绘制原理)》

《Flutter完整开发实战详解(10、 深刻图片加载流程)》

《Flutter完整开发实战详解(11、全面深刻理解Stream)》

《Flutter完整开发实战详解(12、全面深刻理解状态管理设计)》

《跨平台项目开源项目推荐》

《移动端跨平台开发的深度解析》

咱们还会再见吗?
相关文章
相关标签/搜索