阅读 6686

Flutter状态管理 - 初探与总结

最近由于Flutter的大火,加上部门可能会开始尝试在客户端内落地Flutter的项目,因此最近稍微研究了一下Flutter的一些业务技术。

正好最近看了很多关于Flutter状态管理的文章,结合我自己对各个方案的一些想法以及大佬们的一些想法,对各个方案进行了一下总结。

状态管理

flutter的状态管理分为两种:局部状态和全局状态。

局部状态:Flutter原生提供了InheritWidget控件来实现局部状态的控制。当InheritedWidget发生变化时,它的子树中所有依赖它数据的Widget都会进行rebuild。典型的应用场景有:国际化文案、夜间模式等。

全局状态:Flutter没有提供原生的全局状态管理,基本上是需要依赖第三方库来实现。虽然在根控件上使用InheritedWidget也可以实现,不过感觉有点trick.....和React在根节点上使用state有异曲同工之处,会带来同样的问题,比如状态传递过深等。

InheritedWidget

优点:

  1. 自动订阅

InheritedWidget内部会维护一个Widget的Map,当子Widget调用Context#inheritFromWidgetOfExactType时就会自动将子Widget存入Map中,并且将InheritedWidget返回给子Widget。

  1. 自动通知

InheritedWidget重建后悔自动触发InheritElement的Update方法。

缺点:

  1. 无法分离视图逻辑和业务逻辑。
  2. 无法定向通知/指向性通知。

InheritedWidget不会区分Widget是否需要更新的问题,每次更新都会通知所有的子Widget。因此需要配合StreamBuilder来解决问题。

StreamBuilder是Flutter封装好的监听Stream数据变化的Widget,本质上是一个StatefulWidget,内部通过Stream.listen()来监听传入的stream的变化,当监听到有变化时就调用setState()方法来更新Widget。

关于stream的介绍的文章到处都有,别人写的也很详细,这里就不再赘述了。

有了StreamBuilder,我们可以在子Widget上通过StreamBuilder来监听InheritedWidget中的Stream的数据变化,然后判断是否需要更新当前的子Widget,这样就完成了数据的定向通知。

ScopedModel

仓库地址:pub.dartlang.org/packages/sc…

写法上有点类似目前React比较火的@rematch状态管理库,在每个方法中更改完Model数据之后,只需要调用一次notifyListeners()就可以更新全部的状态了。

class CounterModel extends Model {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    // First, increment the counter
    _counter++;
    
    // Then notify all the listeners.
    notifyListeners();
  }
}
复制代码

在入口处也需要将根组件抱在ScopedModel中,这样就可以正常工作了。

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // First, create a `ScopedModel` widget. This will provide 
    // the `model` to the children that request it. 
    return new ScopedModel<CounterModel>(
      model: new CounterModel(),
      child: new Column(children: [
        // Create a ScopedModelDescendant. This widget will get the
        // CounterModel from the nearest ScopedModel<CounterModel>. 
        // It will hand that model to our builder method, and rebuild 
        // any time the CounterModel changes (i.e. after we 
        // `notifyListeners` in the Model). 
        new ScopedModelDescendant<CounterModel>(
          builder: (context, child, model) => new Text('${model.counter}'),
        ),
        new Text("Another widget that doesn't depend on the CounterModel")
      ])
    );
  }
}
复制代码

(此处代码抄自官方Demo)

优点:

  1. 自动订阅
  2. 自动通知
  3. 简单易用,对前端开发者来说学习成本几乎为零

缺点:

  1. 无法分离视图逻辑和业务逻辑
  2. 无法定向通知/指向性通知

ScopedModel其实只是将InheritedWidget简单的封装了一下,因此它继承了InheritedWidget应有的优点和缺点。

Redux

Redux 是 React 中最流行的状态管理工具(之一)。Redux保存了全局唯一的状态书,在业务中通过触发action改变状态,当状态改变时,视图控件也随之更新。Redux解决了状态传递过深的问题,但是因为Dart和js的区别还是很大的,总感觉redux在flutter写起来并不是很舒服...

Redux将数据和视图分离,由数据驱动视图渲染,解决了ScopedModel的视图和业务分离的问题。

  • Store是一个Model类,内部存储了一个state。
  • StoreProvider是一个InheritedWidget,内部存储了一个Store。(数据中心)
  • StoreConnector提供了一个StoreStreamListener,本质上是一个StreamBuilder,它内部有一个Stream<ViewModel>,这个Stream是由Store中的changeController这个SteamController的Stream调用map方法转化来的。。
  • StoreStreamListener通过监听自己的Stream来完成视图的重建。

简单来说就是StoreConnector负责将数据中心的Stream<State>变成Stream<ViewModel>,然后StoreStreamListener负责监听Stream<ViewModel>的变化来更新子Widget。

流程:

  1. View层发出Action
  2. Store中的dispatch将这个action转化成Stream<State>,并添加到changeControllerStream<State>中等待执行。
  3. StoreStreamListener监听到有新的Stream<State>流入,就把流入的State按照业务方事先约定好的covert方法转化成ViewModel,然后将这个ViewModel传入到Stream<ViewModel>中。
  4. View层监听到有新的Stream流入,rebuild整个View。

优点:

  1. 自动订阅
  2. 自动通知
  3. 可以定向通知
  4. 视图和业务逻辑分离

本来看了网上很多文章,感觉在Flutter中使用Redux似乎是一个可行的方案,不过看到公司内部有大佬对Flutter中的Redux的分析文章,确实存在的问题还很多。

因为 Dart 与 JavaScript 直接的区别,Redux 在 Flutter 中使用有许多难以解决的问题

比如通过对比Store构造函数和combineReducers函数:

// dart
Store(
  this.reducer, {
  State initialState,
  List<Middleware<State>> middleware = const [],
  bool syncStream: false,
);
    
Reducer<State> combineReducers<State>(Iterable<Reducer<State>> reducers);
复制代码
// ts
function createStore(reducer: Reducer);
function combineReducers(reducers: ReducersMapObject): Reducer;
复制代码

结合函数原型和平时使用的情况,可以看出二者之间的差异。js中combineReducers传入的值是一个Reducer的映射结构,在函数执行的过程中,每个部分的隐含状态树被整合到一起,成为一颗完整的树。

Dart中没有这样的动态结构,只能在创建Store的时候显示传入所有的初始状态树,这有悖于"解耦"的理念。如果将不同部分的状态存入不同的store的话,这些状态之间的交换又会变得十分困难,这与Redux本身的设计理念不符。

并且在Dart中,immutable数据的创建也十分麻烦,Dart中没有js中对象解构的"..."运算符。

const newState = {
    ...oldState,
    count: count + 1
};
复制代码

基于以上种种原因,虽然开始比较看好Redux,最后我还是放弃了使用Redux...

BloC

BloC的核心思想是数据与视图分离,由数据变化驱动试图渲染。(没错和redux一模一样)

从某种意义上讲Redux可以看做是一种特殊的BloC。

介绍文档已经有大佬在掘金发表过了:juejin.im/post/684490… 我也是看这篇文章学习的BloC的相关知识。

BloC和Redux的区别在于:redux有一个数据中心store才存放所有的数据,当数据改变时由数据中心调用covert方法将state转换成对应的ViewModel,然后通知子Widget进行修改。

而在BloC中则没有store的概念,只有一个StreamController,但是这个Controller并不存放数据,只是处理数据的,并且BloC没有convert方法,Viwe会直接将State转换成ViewModel。

在Redux的优点的基础上,BloC将业务分离地更加彻底,并且解决了Redux难以分离各个部分状态的痛点,一个应用程序可以有多个数据源,并且可以通过流操作对其进行加工组合,具有较强的扩展性,加上Dart原生支持Stream类,书写起来也比较方便。

在业务中使用BloC方案时,不需要我们重新用Stream实现这一套方案,可以直接使用flutter_bloc库即可:github.com/felangel/bl…

reBloC

地址:github.com/RedBrogdon/… rebloc是redux+bloc的一个实现方案。

Rebloc is an attempt to smoosh together two popular Flutter state management approaches: Redux and BLoC. It defines a Redux-y single direction data flow that involves actions, middleware, reducers, and a store. Rather than using functional programming techniques to compose reducers and middleware from parts and wire everything up, however, it uses BLoCs.

The store defines a dispatch stream that accepts new actions and produces state objects in response. In between, BLoCs are wired into the stream to function as middleware, reducers, and afterware. Afterware is essentially a second chance for Blocs to perform middleware-like tasks after the reducers have had their chance to update the app state.

官方的说明是想要结合redux的数据流(解决指向性通知)方案以及bloc的响应式编程的更少的编码量。 但是感觉这个方案既拥有redux的复杂性,又引入了bloc的闭环stream流,最终导致整个方案更加复杂了。

fish_redux

fish_redux是阿里咸鱼开源的一套flutter设计方案,介绍:zhuanlan.zhihu.com/p/55062930 也是基于redux进行了一下改良封装,多了几个新的概念:Adapter、Component。

redux本身只提供一种全局状态管理方案,并不关心具体业务。fish_redux是针对业务方对redux又进行了一次使用层面的改良。

每个组件(Component)需要定义一个数据(Struct)和一个Reducer。同时组件之间的依赖关系解决了集中和分治的矛盾。

Component Component的概念有点类似我们rematch中的model,含有View、Effect、Reducer三部分。 View负责展示 Effect负责非state修改的函数 Reducer负责修改state的函数

Adapter 由于Flutter中ListView的高频使用,fish_redux对ListView做了性能优化,Adapter由此出现。

它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题:

  1. 将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化。
  2. Component 无法区分 appear|disappear 和 init|dispose 。
  3. Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。 概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。 做出这样独立一层的抽象是, 我们看实际的效果, 我们对页面不使用框架,使用框架 Component,使用框架 Component+Adapter 的性能基线对比

fish_redux目录结构:

  • page
  • --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

优点:

  1. 数据集中管理,框架自动完成reducer合并。
  2. 组件分治管理,组件之间以及和容器之间互相隔离。
  3. View、Reducer、Effect隔离。易于编写复用。
  4. 声明式配置组装。
  5. 良好的扩展性。

个人感觉fish_redux的设计适用于复杂的业务场景,加上复杂的目录结构以及相关概念,不太适合普通的数据不太复杂的业务。

Mobx

地址:pub.dev/packages/mo…

与BloC类似,MobX也是观察者模式。但是MobX将所有的更新和消息推送都隐藏在了getter和setter里面,因此开发者在使用的时候无需关心消息发送和响应的时机,组件会在任何它依赖的对象更新时进行重新渲染。

Dart版本的MobX使用起来和js很像,由于Dart没有装饰器,因此MobX使用了mobx_codegen生成部分代码代替了这部分的工作:github.com/mobxjs/mobx…

import 'package:mobx/mobx.dart';

// Include generated file
part 'todos.g.dart';

// This is the class used by rest of your codebase
class Todo = TodoBase with _$Todo;

// The store-class
abstract class TodoBase implements Store {
  TodoBase(this.description);

  @observable
  String description = '';

  @observable
  bool done = false;
}
复制代码

(以上代码抄自官方Demo)

以上创建了一个包含响应式状态以及对应方法的类。使用mobx_codegen生成的_$Todo中继承了descriptiondone属性,并且给他们加上了额外的操作,使得状态可以被捕获。

这里是一个官方Counter的Demo:

// package:mobx_examples/counter/counter.dart
import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter; // 这里是mobx_codegen生成的

abstract class CounterBase implements Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}
复制代码
// counter_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx_examples/counter/counter.dart';

class CounterExample extends StatefulWidget {
  const CounterExample();

  @override
  CounterExampleState createState() => CounterExampleState();
}

class CounterExampleState extends State<CounterExample> {
  final Counter counter = Counter();

  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Observer(builder: (_) => Text('${counter.value}')),
      RaisedButton(
        child: Text('inc'),
        onPressed: counter.increment,
      )
    ]);
  }
}
复制代码

当按钮被点击时,increment方法就会被触发,从而改变value值,然后上报变化。Observer收到更新消息后会重新渲染组件。根据 MobX 的原理,可以发现上面提出的问题都可以被解决。组件的更新与数据引用是否改变完全没有关系,它只关心使用的值是否发生了改变,因此完全不需要考虑 immutable 的问题。

MobX 还有一个其它方案较难实现的优点,它可以以很低的代价创建很多相同结构的状态以及相应的操作。例如在维护一个列表时,可以在每一个列表项的组件中创建一个 MobX 的对象,然后让里面的子组件响应这个对象的更新操作。以上面的组件为例,无论创建多少个 CounterExample 对象,都会有相应的 Counter 在里面,不需要把这些状态以一种不自然的方式组合到一起。另外如果有其它类型的组件也有类似的状态和操作,也可以使用这个类,减少重复的开发。这正是 React Hooks 想要解决的问题,在此之前,原生的 React 没有比较合适的方法处理这种场景(在 Dart 中也可以使用 mixin 得到类似的效果)。Redux 需要处理状态数组或者状态的字典,这是一个比较复杂的操作,尤其是在 Dart 环境下。BloC 的方式依赖于 InheritWidget 获取上下文,如果有多个相同类型的状态对象在组件树中容易引发冲突,需要使用额外的方法解决这个问题。

总结

与 React 类似,Flutter 可以使用 setState 管理组件局部的状态,但是很难仅仅使用 setState 来管理整个复杂应用。

在上面提到的状态管理方案中,感觉比较好用的只有BloC和MobX,如果习惯vue和MobX的同学建议直接使用MobX,MobX的数据响应几乎透明,开发者可以更加自由地组织自己想要的状态。

Flutter目前感觉还太年轻,状态管理方案各家也都在探索,像咸鱼自己出的fish_redux,社区内还没有一个比较完美的状态管理方案,根据自己的业务选择合适的状态管理方案应该是最好的答案了。

(如果文章中有Stream打成了Steam请忽略QAQ)