Flutter状态管理 - 初探与总结
栏目: JavaScript · 发布时间: 5年前
内容简介:最近由于Flutter的大火,加上部门可能会开始尝试在客户端内落地Flutter的项目,因此最近稍微研究了一下Flutter的一些业务技术。正好最近看了很多关于Flutter状态管理的文章,结合我自己对各个方案的一些想法以及大佬们的一些想法,对各个方案进行了一下总结。flutter的状态管理分为两种:局部状态和全局状态。
最近由于Flutter的大火,加上部门可能会开始尝试在客户端内落地Flutter的项目,因此最近稍微研究了一下Flutter的一些业务技术。
正好最近看了很多关于Flutter状态管理的文章,结合我自己对各个方案的一些想法以及大佬们的一些想法,对各个方案进行了一下总结。
状态管理
flutter的状态管理分为两种:局部状态和全局状态。
局部状态:Flutter原生提供了InheritWidget控件来实现局部状态的控制。当InheritedWidget发生变化时,它的子树中所有依赖它数据的Widget都会进行rebuild。典型的应用场景有:国际化文案、夜间模式等。
全局状态:Flutter没有提供原生的全局状态管理,基本上是需要依赖第三方库来实现。虽然在根控件上使用InheritedWidget也可以实现,不过感觉有点trick.....和React在根节点上使用state有异曲同工之处,会带来同样的问题,比如状态传递过深等。
InheritedWidget
优点:
- 自动订阅
InheritedWidget内部会维护一个Widget的Map,当子Widget调用Context#inheritFromWidgetOfExactType时就会自动将子Widget存入Map中,并且将InheritedWidget返回给子Widget。
- 自动通知
InheritedWidget重建后悔自动触发InheritElement的Update方法。
缺点:
- 无法分离视图逻辑和业务逻辑。
- 无法定向通知/指向性通知。
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)
优点:
- 自动订阅
- 自动通知
- 简单易用,对前端开发者来说学习成本几乎为零
缺点:
- 无法分离视图逻辑和业务逻辑
- 无法定向通知/指向性通知
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。
流程:
- View层发出Action
- Store中的dispatch将这个action转化成
Stream<State>
,并添加到changeController
的Stream<State>
中等待执行。 - StoreStreamListener监听到有新的
Stream<State>
流入,就把流入的State按照业务方事先约定好的covert方法转化成ViewModel,然后将这个ViewModel传入到Stream<ViewModel>
中。 - View层监听到有新的Stream流入,rebuild整个View。
优点:
- 自动订阅
- 自动通知
- 可以定向通知
- 视图和业务逻辑分离
本来看了网上很多文章,感觉在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/5bb6f3… 我也是看这篇文章学习的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 个问题:
- 将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化。
- Component 无法区分 appear|disappear 和 init|dispose 。
- 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
优点:
- 数据集中管理,框架自动完成reducer合并。
- 组件分治管理,组件之间以及和容器之间互相隔离。
- View、Reducer、Effect隔离。易于编写复用。
- 声明式配置组装。
- 良好的扩展性。
个人感觉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
中继承了 description
和 done
属性,并且给他们加上了额外的操作,使得状态可以被捕获。
这里是一个官方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)
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。