Flutter训练营(七)-Flutter状态管理

641 阅读12分钟

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件,同时也是当下最流行的跨端解决方案。

前言

在弄清楚如何使用 各个状态管理库 之前,我们首先需要清楚地知道为什么我的应用需要状态管理。

当开发的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将数据映射成视图就可以了。你可能并不需要状态管理,但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。

随着应用需要共享多处统一状态时,我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。

Flutter 实际上在一开始就为我们提供了一种状态管理方式 — StatefulWidget。然而我们发现它仅适合用于在单个 Widget 内部维护其状态。当我们需要使用跨组件的状态时,StatefulWidget 将不再是一个好的选择。

State 属于某一个特定的 StatefulWidget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,很容易就增大代码耦合度。

这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,因此,各大状态管理框架应运而生。本文主要介绍2种:官方推荐的Provider、阿里团队的 Fish-Redux

一、状态管理

了解过前端的同学应该对Vue、React的状态管理不陌生,我之前的文章也有详细比较过,常见的状态管理有Vuex、Flux、Redux、MobX、Bloc、Stamen等等。但是对于新生代的Flutter,之前还没有完全符合Flutter的状态管理库,之前介绍过Redux是一门JS库,Redux 是一个函数式的数据管理的框架,不仅仅可以用于React,还可以应用于其他框架和平台,特点是、可预测、集中式、易调试、灵活性的数据管理的框架,所有对数据的增删改查等操作都由 Redux 来集中负责。

image.png

但是对于Flutter来说,Redux 的优点同时也是缺点,Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它。所以在Flutter中使用 Redux 中将面临两个具体问题:Redux 的集中和 Component 的分治之间的矛盾;Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。

就在今天(2019年3月5号),闲鱼宣布在 GitHub 上开源 Fish Redux,从此Flutter有了真正意义上的完善的状态管理框架-Fish Redux。

二、旧-Flutter-provide

上面说到Fish Redux 算是Flutter真正意义上完善的状态管理框架,原因是之前确实也有状态管理-Flutter-provide。(官方地址 github.com/google/flut…

Provide 和 Scoped_model一样,也是借助了InheritWidget,将共享状态放到顶层MaterialApp之上。底层部件通过Provier获取该状态,并通过混合ChangeNotifier通知依赖于该状态的组件刷新。

举个例子:

image.png

这两个页面都同时依赖于counter 和 switcher两个不同的状态。并且一个页面改变状态之后另外一个页面状态也随之改变。

1.添加依赖

在pubspec.yaml中添加Provide的依赖。

dependencies:
  provide: ^1.0.2

2.创建Model

这里实际上它承担了State的职责,但是为了和官方的State区分所以叫做model。

import 'package:flutter/material.dart';

class Counter with ChangeNotifier{
  int value = 0;

  increment(){
    value++;
    notifyListeners();
  }
}

这里我们可以看到,数据和操作数据的方法都在model中,我们可以很清晰的把业务分离出来。对比Scoped_model可以发现,Provide模式中model不再需要继承Model类,只需要实现Listenable,我们这里混入ChangeNotifier,可以不用管理听众。通过 notifyListeners 我们可以通知听众刷新。

3.将状态放入顶层

void main() {
  var counter = Counter();
  var providers = Providers();

//将counter对象添加进providers
  providers.provide(Provider.value(counter));

  runApp(
    ProviderNode(
        child: MyApp(), 
        providers: providers),
    );
}

ProviderNode 封装了 InheritWidget,并且提供了一个 providers 容器用于放置状态。providers 内部通过 Map> 来储存 provider,在存放的时候你可以通过传入ProviderScope("name") 来指定key。Provider.value 将 counter 包装成了_ValueProvider。并在它的内部提供了 StreamController 从而实现对数据进行流式操作。

4.获取状态

同样的Provide也提供了两种获取State的方法。我们先来介绍第一种,通过Provide小部件获取。

Provide(
  builder: (context, child, counter) {
     return Text(
        '${counter.value}',
        style: Theme.of(context).textTheme.display1,
      );
   },
),

每次通知数据刷新时,builder将会重新构建这个小部件。builder方法接收三个参数,这里主要介绍第二个和第三个。第二个参数child:假如这个小部件足够复杂,内部有一些小部件是不会改变的,那么我们可以将这部分小部件写在Provide的child属性中,让builder不再重复创建这些小部件,以提升性能。第三个参数counter:这个参数代表了我们获取的顶层providers中的状态。

scope:通过指定ProviderScope获取该键所对应的状态。在需要使用多个相同类型状态的时候使用。

第二种获取方式:Provide.value(context)final currentCounter = Provide.value(context);这种方式实际上调用了 context.inheritFromWidgetOfExactType 找到顶层的 _InheritedProviders 来获取到顶层 providers 中的状态。


如何组织多个状态

和 scoped_model 不同的是,provide 模式中你可以轻松组织多个状态。只需要将状态provide 进 provider 中就可以了。

void main() {
  var counter = Counter();
  var switcher = Switcher();

  var providers = Providers();

  providers
    ..provide(Provider.value(counter))
    ..provide(Provider.value(switcher));

  runApp(
    ProviderNode(
        child: MyApp(), 
        providers: providers)
    );
}

获取数据流

在将 counter 添加进 providers 的过程中进行了一次包装。我们刚才通过分析源码知道了这个操作能够让我们处理流式数据。通过 Provide.stream(context) 就能获取数据流。

StreamBuilder(
   initialData: currentCounter,
   stream: Provide.stream(context)
       .where((counter) => counter.value % 2 == 0),
   builder: (context, snapshot) =>
       Text('Last even value: ${snapshot.data.value}')),

三、新-Fish Redux

Fish Redux 是闲鱼团队基于 Redux 做的一次量身改良,通过 Redux 做集中化的可观察的数据管理。FR 是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用,对于传统 Redux 在使用层面上的两大缺点做了重大改良,具体做法是:首先规定一个组件需要定义一个数据(Struct)和一个 Reducer,同时组件之间存在着父依赖子的关系。通过这层依赖关系去解决了 集中 和 分治 之间的矛盾,而对 Reducer 的手动层层 Combine 变成由框架自动完成,使之简化了使用 Redux 的困难,同时也得到了理想的集中的效果和分治的代码。

sample_page
- State
- Action
- Reducer
- Store
- Middleware

一个组件页面目录结构可以是这样设计,好处是这些概念与 ReduxJS 是一致的,可以保留Redux 的优势。Fish Redux 最显著的特征是函数式的编程模型、可预测的状态管理、可插拔的组件体系、最佳的性能表现。它的特点是配置式组装。一方面我们将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现;另一方面将Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。

Fish Redux 的灵感主要来自于 Redux, Elm, Dva 这样的优秀框架。所以用闲鱼Flutter团队自己的话来说,Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。

四、FR源码解读

1. Action

Action 包含两个字段

  • type

  • payload

推荐的写法是为一个组件或适配器创建一个 action.dart 文件,包含两个类

  • 为 type 字段起一个枚举类

  • 为 Action 的创建起一个 ActionCreator 类,这样利于约束 payload 的类型。

  • Effect 接受处理的 Action,以 on{ Verb } 命名

  • Reducer 接受处理的 Action,以 { verb } 命名

enum MessageAction {
    onShare,
    shared,
}

class MessageActionCreator {
    static Action onShare(Map<String, Object> payload) {
        return Action(MessageAction.onShare, payload: payload);
    }

    static Action shared() {
        return const Action(MessageAction.shared);
    }
}

2. Adapter

我们在基础 Component 的概念外,额外增加了一种组件化的抽象 Adapter。它的目标是解决 Component 模型在 ListView 的场景下的 3 个问题:

  • 1)将一个"Big-Cell"放在 ListView 里,无法享受 ListView 代码的性能优化。

  • 2)Component 无法区分 appear|disappear 和 init|dispose 事件。

  • 3)Effect 的生命周期和 View 的耦合,在 ListView 的有些场景下不符合直观的预期。

一个 Adapter 和 Component 几乎都是一致的,除了以下几点:

  • Component 生成一个 Widget,Adapter 生成一个 ListAdapter,ListAdapter 有能力生成一组 Widget。

  • 不具体生成 Widget,而是一个 ListAdapter,能非常大的提升页面帧率和流畅度。

  • Effect-Lifecycle-Promote

  • Component 的 Effect 是跟着 Widget 的生命周期走的,Adapter 的 Effect 是跟着上一级的 Widget 的生命周期走。

  • Effect 提升,极大的解除了业务逻辑和视图生命的耦合,即使它的展示还未出现,的其他模块依然能通过 dispatch-api,调用它的能力。

  • appear|disappear 的通知

  • 由于 Effect 生命周期的提升,我们就能更加精细的区分 init|dispose 和 appear 或disappear。而这在 Component 的模型中是无法区分的。

  • Reducer is long-lived, Effect is medium-lived, View is short-lived.

3. Auto-Dispose

它是一个非常简易管理生命周期对象的方式。一个 auto-dispose 对象可以自我主动释放,或者在它 follow 的 托管对象释放的时候,释放。在 Effect 中使用的 Context,以及 HigherEffect 中的 EffectPart,都是 auto-dispose 对象。所以我们可以方便的将自定义的需要做生命周期管理的对象托管给它们。

class ItemWidgetBindingObserver extends WidgetsBindingObserver
    with AutoDispose {
  ItemWidgetBindingObserver() : super() {
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (AppConfig.flutterBinding.framesEnabled &&
        state == AppLifecycleState.resumed) {
      AppConfig.flutterBinding.performReassemble();
    }
  }

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
  }
}

void _init(Action action, Context<ItemPageContainerState> ctx) {
    final ItemWidgetBindingObserver observer = ItemWidgetBindingObserver();
    observer.follow(ctx);
}

4. Connector

它表达了如何从一个大数据中读取小数据,同时对小数据的修改如何同步给大数据,这样的数据连接关系。它是将一个集中式的 Reducer,可以由多层次多模块的小 Reducer 自动拼装的关键。它大大降低了我们使用 Redux 的复杂度。我们不再关系组装过程,我们关系核心的什么动作促使数据怎么变化。它使用在配置 Dependencies 中,在配置中我们就固化了大组件和小组件之间的连接关系(数据管道),所以在我们使用小组件的时候是不需要传入任何动态参数的。

class DetialState {
    Profile profile;
    String message;
}

Connector<DetialState, String> messageConnector() {
    return Connector<DetialState, String>(
        get: (DetialState state) => state.message,
        set: (DetialState state, String message) => state.message = message,
    );
}

5. Component

组件是对视图展现和逻辑功能的封装。

面向当下,从 Redux 的视角看,我们对组件分为状态修改的功能(Reducer)和其他。面向未来,从 UI-Automation 的视角看,我们对组件分为展现表达和其他。结合上面两个视角,于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的操作。我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。

所以有了这个公式Component = View + Effect(可选) + Reducer(可选) + Dependencies ( 可选 )

分治:从组件的角度:

image.png

集中:从 Store 的角度:

image.png


6. CustomAdapter

对大 Cell 的自定义实现,要素和 Component 类似,不一样的地方是 Adapter 的视图部分返回的是一个 ListAdapter。

class CommentAdapter extends Adapter<CommentState> {
    CommentAdapter()
        : super(
            adapter: buildCommentAdapter,
            effect: buildCommentEffect(),
            reducer: buildCommentReducer(),
        );
}

ListAdapter buildCommentAdapter(CommentState state, Dispatch dispatch, ViewService service) {
    final List<IndexedWidgetBuilder> builders = Collections.compact(<IndexedWidgetBuilder>[]
    ..add((BuildContext buildContext, int index) =>
        _buildDetailCommentHeader(state, dispatch, service))
    ..addAll(_buildCommentViewList(state, dispatch, service))
    ..add(isEmpty(state.commentListRes?.items)
        ? (BuildContext buildContext, int index) =>
            _buildDetailCommentEmpty(state.itemInfo, dispatch)
        : null)
    ..add(state.commentListRes?.getHasMore() == true
        ? (BuildContext buildContext, int index) => _buildLoadMore(dispatch)
        : null));
    return ListAdapter(
    (BuildContext buildContext, int index) =>
        builders[index](buildContext, index),
    builders.length,
    );
}

7. Dependencies

Dependencies 是一个表达组件之间依赖关系的结构。它接收两个字段

  • slots

  • string

它主要包含三方面的信息

  • slots,组件依赖的插槽。

  • adapter,组件依赖的具体适配器(用来构建高性能的 ListView)。

  • Dependent 是 subComponent 或 subAdapter + connector 的组合。

  • 一个 组件的 Reducer 由 Component 自身配置的 Reducer 和它的 Dependencies 下的所有子 Reducers 自动复合而成。

// register in component
class ItemComponent extends ItemComponent<ItemState> {
  ItemComponent()
      : super(
          view: buildItemView,
          reducer: buildItemReducer(),
          dependencies: Dependencies<ItemState>(
            slots: <String, Dependent<ItemState>>{
              'appBar': AppBarComponent().asDependent(AppBarConnector()),
              'body': ItemBodyComponent().asDependent(ItemBodyConnector()),
              'ad_ball': ADBallComponent().asDependent(ADBallConnector()),
              'bottomBar': BottomBarComponent().asDependent(BottomBarConnector()),
            },
          ),
        );
}

// call in view
Widget buildItemView(ItemState state, Dispatch dispatch, ViewService service) {
  return Scaffold(
      body: Stack(
        children: <Widget>[
          service.buildComponent('body'),
          service.buildComponent('ad_ball'),
          Positioned(
            child: service.buildComponent('bottomBar'),
            left: 0.0,
            bottom: 0.0,
            right: 0.0,
            height: 100.0,
          ),
        ],
      ),
      appBar: AppbarPreferSize(child: service.buildComponent('appBar')));
}

8. Dependent

Dependent = connector+ subComponent | subAdapter 的组合,它表达了小组件|小适配器是如何连接到 Component 的。


9. Directory

推荐的目录结构会是这样:

sample_page
    -- action.dart
    -- page.dart
    -- view.dart
    -- effect.dart
    -- reducer.dart
    -- state.dart
    components
        sample_component
        -- action.dart
        -- component.dart
        -- view.dart
        -- effect.dart
        -- reducer.dart
        -- state.dart

上层负责组装,下层负责实现。

五、Fish Redux的优化

Redux 是一个专注于状态管理的框架;Fish Redux 是基于 Redux 做状态管理的应用框架;应用框架不仅仅要解决状态管理的问题,还要解决分治,通信,数据驱动,解耦等等问题。

Redux 通过使用者手动组织代码的形式来完成从小的 Reducer 到主 Reducer 的合并过程;Fish Redux 通过显式的表达组件之间的依赖关系,由框架自动完成从细力度的 Reducer 到主 Reducer 的合并过程;

Fish Redux 提供了一个 Adapter 的抽象组件模型,在基础的组件模型以外,Fish Redux 提供了一个 Adapter 抽象模型,用来解决在 ListView 上大 Cell 的性能问题。通过上层抽象,可以得到了逻辑上的 ScrollView,性能上的 ListView。

六、MobX库

相信当大家开始用 Flutter 后,大多数项目都是在 Flutter 中编写的。终究有一天会遇到 setState() 这座大山,想逃都逃不掉。它会同时处理很多类,带着一大堆动态数据,让代码变得丑陋不堪,写起来也像蜗牛一样慢;而且它会严重拖累应用程序的性能,因为你得不停从头至尾重建小部件树,哪怕变量值稍微改变一下也得折腾一次。

前面介绍了Flutter-provide、Fish Redux这两种状态管理库,对Fish Redux感兴趣的同学可以参考我之前的分析文章:Fish-Redux 的设计原则(上篇)Fish-Redux 的设计原则(下篇)。 今天介绍一下MobX 。。。嗯??? 这不是前端react常用的状态管理 mobX,对,又是它,它现在也推出了MobX Flutter。MobX 是一个广受好评的库,它融入函数响应式编程(TFRP)原则简化了状态管理,使其容易扩展。

参考文献

阿里巴巴 fish-redux 地址:github.com/alibaba/fis…

MobX Flutter : pub.dev/packages/mo…