Flutter中状态管理的理解
一、状态管理认知
1、核心解耦
我理解的状态管理核心思想是解耦。 和别的框架一样,
Flutter
中的状态管理主要是对逻辑、数据等进行解耦,以适应复杂业务的开发及维护。
2、对比其它框架
最经典的解耦就是MVC了,划分为三个模块:
- Model层:数据层
- View层:页面UI
- Controller:逻辑处理
现在很多也在用这一经典的模式,MVC
模式可以解决大部分的耦合问题,但也存在Controller
过于臃肿的情况,所以后面发展而来了MVVM
、MVP
等模式,用于在不同的场景下更好的解耦。
3、Flutter状态管理
Flutter
中的状态管理主要是对逻辑层
、状态层
、页面层
、行为层
等进行划分:
- view:界面层,主要是UI
- Logic:逻辑层,主要处理业务逻辑
- State:状态层,主要处理页面所需数据状态
- Action:行为层,主要处理交互事件
- Reducer:这个层级,是专门用于处理数据变化的
不同的解耦层可以组合成不同的模式,如MVC
是由逻辑层、数据层、页面层组成,在Flutter中可以分为极简模式(页面层+逻辑层)、标准模式(页面层+逻辑层+状态层)、严格模式(页面层+逻辑层+状态层+行为层)、强迫症模式(页面层+逻辑层+状态层+行为层+ Reducer层),几种模式各有优缺点,下面我会结合具体的状态管理框架
详细的讲下这几种模式。
二、状态管理模式
2.1、极简模式
状态管理-极简模式.png从上图可以看出极简模式由逻辑层
+页面层
组成,各自的职责如图所示,非常的简洁,对于不是太复杂的业务使用与用该模式开发,如果一些简单的页面也用复杂的模式感觉有点生搬硬套了,有的分层完全是不需要的,也造成了内存的浪费。
现在较为流行的状态管理框架也是按照极简模式划分的,如provider
、getx
,两者的区别不大,不过在选择使用时选provider
可能需要能hold的住InheritedWidget
,选getx
可能需要能hold的住依赖注入
,回收GetXController
。
2.2、标准模式
状态管理-标准模式.png从上图可以看出标准模式由 逻辑层
+页面层
+状态层
组成,各自的职责如图所示,相比极简模式,标准格式多了状态层,状态层主要是存储所需的状态变量。标准模式类似于经典的MVC
的分层,用的十分普遍,能够很好的解耦。
常见的框架有cubit
、provider
和getx
。
2.3、严格模式
状态管理-严格模式.png从上图可以看出标准模式由 逻辑层
+页面层
+状态层
+行为层
组成,各自的职责如图所示,相比标准模式,严格格式多了一个行为层,行为层从图中可以清楚的看出主要是为了处理交互事件
,那为什么要多出这一层呢?其实在Flutter中使用标准模式时会存在一个问题,很多的交互事件都是深埋在各个widget中,查找起来非常不变,事件少了还好,如果后续越来越复杂事件越来越多那就成了一团麻了。有了行为性这一分层,可以很好的处理交互事件,页面有什么交互事件,交互事件如何处理只需到Action
文件中查找即可,对于后期的维护来说十分方便。
常见的状态管理框架:Bloc
、Redux
和fish_redux
。
2.4、强迫症模式
状态管理-强迫症模式.png从上图可以看出标准模式由 逻辑层
+页面层
+状态层
+行为层
+Reducer层
组成,各自的职责如图所示,相比严格模式,强迫症模式多了一个Reducer层
,Reducer层
主要是对数据进行处理并更新后刷新页面。从上图来看这个结构已经有点复杂了,为了解耦数据刷新这一层次,付出了巨大的成本
常见的状态管理框架:Redux
、fish_redux
。
三、状态管理框架对比
3.1、Bloc
BLoC
是谷歌提出的一种设计模式,利用stream
流的方式实现界面的异步渲染和重绘,我们可以非常顺利的通过BLoC实现业务与界面的分离。在使用BLoC
前需要了解三个重要的概念,分别是 Cubit
、 BlocObserver
和BLoC
。
3.1.1、Cubit
Cubit
是管理状态数据的BlocBase
子类,它可以管理任意类型的数据,包括基本类型到复杂对象。在Cubit调用emit
构建新的状态数据前需要给状态数据一个初始值。当状态数据发生改变的时候,会触发 Cubit 的onChange
回调,如果出现错误则会触发onError
回调。
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
3.1.2、BlocObserver
BlocObserver
可以监听所有的Cubit
的变化,从而使得我们可以同时监听多个Cubit
。
3.1.3、Bloc
Bloc
也是继承 BlocBase
的类,但相比 Cubit
来说更为高级一些。它使用的是 events
而不是暴露的函数来更新状态。在 Bloc
内部,有一个onEvent
方法,在这个方法里,通过 EventTransformer
将 event
转换为更新状态的方法来刷新状态数据。每个event
都可以有对应的 EventHandler
来处理该 event,完成后再通过 emit
触发通知状态更新。当状态转变前会调用 onTransition
,在这里会有当前的状态,触发更新的 event 和下一个状态。
从
Bloc
的设计来看,使用了函数Cubit形式
和事件Bloc形式
的方式来驱动状态数据更新,然后再通过emit
通知状态更新,通过这种方式解耦 UI 界面和业务逻辑。
优缺点分析:
优点:
-
BLoC
的目录结构清晰,完全符合mvvm
的习惯。对于工程化项目来说会比较受欢迎,团队协作起来会减少出错的概率,大家都跟着一个模式去做,维护性也提高了; -
BLoC
的目录结构清晰,完全符合mvvm
的习惯。对于工程化项目来说会比较受欢迎,团队协作起来会减少出错的概率,大家都跟着一个模式去做,维护性也提高了;
缺点:
-
BLoC
使用起来相对复杂,需要创建多个文件。虽然官方引入了cubit
,把event
组合到bloc文件中,但强烈的结构化依然让不少初学者难以入门; -
颗粒度
的把控相对困难。通过BlocBuilder
构建的视图,在state变更时,视图都会rebuild,想要控制颗粒度只能把bloc再拆细,这会极大的增加代码复杂度和工作量;不过这个问题可通过引入freezed生成代码,然后通过buildWhen等方式减少视图刷新的频次。
3.2、Provider
Provider
是Flutter官方开发维护的,也是近些年官方最为推荐的状态管理库。它对InheritedWidget
进行了上层封装,致力解决原生setState方案的props臃肿、展示与逻辑耦合问题。使用时需在项目中引入provider
这个库。
Provider将页面分为业务和视图两层,并定义Notifier
、Consumer
两个核心概念:Notifier负责实现业务逻辑,且在数据更新时发出通知。Consumer负责实现界面逻辑,并在数据更新时更新自身,以及用户交互时调用Notifier方法。
优缺点分析:
优点:
- 基于官方InheritedWidget的封装,不存在任何风险,很稳定且不会给性能方面加负担;
- 方案涉及概念少,上手成本低;
缺点:
- 数据流分为业务、视图两层。项目规模变大时,业务层复杂度容易指数级增长;
-
context
强关联,有Flutter开发经验的都知道,context大多时候基本都是在widget中才能获取到,在其他地方想随时获取 BuildContext 是不切实际的,也就意味着大多时候只能在view层去获取到Provider提供的信息。
3.3、GetX
GetX
是 Flutter 上的一个轻量且强大的解决方案,也是我现在项目中正在使用的框架,在Flutter状态管理中绝对算是异军突起,一经发布就因其简单且全面的优势,引得一大批簇拥者。GetX可以称之为全家桶式的框架,具有以下多种功能:
- 主题切换:比如深色模式切换;
- 多语言:可以通过配置 Map 搞定多语言;
- 弹窗提醒:包括了 SnackBar 和对话框;
- 路由:
无需
context 的路由跳转; - 离线存储:不依赖原生的key-value 存储组件的离线存储
GetStorage
; - 状态管理:快速接入的响应式状态管理;
- 工具类:例如表单验证工具,获取系统参数(平台类型,屏幕尺寸等);
- 依赖注入容器:使用简单的
put
和find
方法完成容器对象的注册和获取; - 网络请求:可以使用 GetConnect 完成网络请求。
3.3.1、路由管理
GetX
内部实现了路由管理,这个是非常重要的,这样我们就不需要使用其他第三插件,而且GetX
的路由管理非常简单,代码也简洁。
/// 跳转新页面
/// 第一种方式 进入新页面 直接页面
Get.to(TestPage());
/// 第二种方式 进入新页面 配置路由名称 建议这种统一配置
Get.toNamed(Routes.TestPage);
/// 弹出当前页,并将一个新的[page]推入堆栈,就是删除就页面,使用新页面
Get.off(TestPage());
/// Push a [page]和弹出几个页面在堆栈中,就是进入新页面,删除之前进栈的页面。比如场景(注册-手机号-其他注册信息-注册完了直接到主页,之前页面全部删掉。)
Get.offAll(TestPage());
/// 同上,就是传路由名称
Get.offAllNamed(FXRoutes.TestPage);
/// 返回上一面 就一句
Get.back()
3.3.2、状态管理
使用getx的状态管理,我一般一个page
对应一个logic
, logic需要继承 GetxController
,该logic来处理逻辑并控制page,目录如下:
将页面和logic管理起来GetX使用的是依赖注入,有两种方式可以实现:
1、在view中使用依赖注入和logic关联,然后在获取到logic中的state,从而view中的交互事件直接调取logic中的方法进行处理,state用于刷新页面UI。
final logic = Get.put(TestPageLogic());
final state = Get.find<TestPage>().state;
2、在路由中绑定
GetPage(
name: FXRoutes.TestPage,
page: () => TestPage(),
/// 主要代码是这个 绑定
binding: BindingsBuilder(() => {
Get.lazyPut<TestPageLogic>(
() => TestPageLogic())
})),
使用GetView可以直接使用logic
/// 页面继承GetView<> 传依赖注入的控制器 这样就可以直接使用了,会发现这边没有 Get.put,或者Git.find, 使用的时候直接logic。 看源码可以知道GetView内部已经帮我们实现了。
class TestPage extends GetView<TestPageLogic> {
@override
Widget build(BuildContext context) {
}
}
优缺点分析:
优点:
- 使用简单,用起来确实很简单,极易上手;脱离context,随时随地想用就用,解决了BLoC和Provider的痛点;
- 全家桶式功能,使用GetX后,我们无需再单独去做路由管理、国际化、主题、全局context等;
缺点:
- 使用 GetX 的导航需要使用自定义的 MaterialApp 或 CupertinoApp,也就是我们需要使用 GetMeterialApp 或 GetCupertinoApp 包裹应用才能够在页面跳转时无需使用 BuildContext。对应用的侵入性比较强;
- 使用GetX的话需要能hold住依赖注入。
3.4、fish_redux
fish_redux
是阿里咸鱼团队开发的一个状态管理框架,是基于 Redux 数据管理的组装式 flutter 应用框架, 它特别适用于构建中大型的复杂应用。
它的特点是配置式组装。 一方面我们将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现; 另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数,结构非常清晰,适合团队开发,易与后期的维护。
使用fish_redux进行开发,拆分的文件目录为:
- action.dart 事件转发动作类;
- effect.dart 网络请求、逻辑处理;
- page.dart 做一些配置工作;
- reducer.dart 简单的对数据的操作及更新;
- state.dart 数据管理类;
- view.dart 视图类。
在开发中可下载 FishReduxTemplate
插件,可快速生成对应的文件:
fish_redux相比其它框架多了action
和reducer
层,其中action层主要是行为层,统一转发处理交互事件,我认为这一层还是很有必要的,多了一层从框架端来说复杂度是提高了,但是对于使用者来说却是结构清晰了,如果没有行为层,交互事件大多都埋在各种widget
中,这样找起来非常不方便,而且随着后期的迭代也会变得越来越难维护,有了行为层就清晰多了。对于reducer
层我保留意见,可能是水平达不到,我没有感受到这层妙处在哪。
优缺点分析:
优点:
- 结构清晰,适合团队开发,也利于后期维护;
缺点:
- 更新速度较差,我们项目已放弃fish_redux,因为3.0版本还没有适配空安全;
- 结构会较为复杂,上手难度大,学习成本较高。
[注]:选择哪种状态管理框架没有准确的答案,其受难易程度、可维护性、开发成本、性能、应用场景、团队技术栈等因素的影响,所以适合自己的才是最好的。