Flutter中状态管理的理解

aykff-l7m6b.png

一、状态管理认知

1、核心解耦

我理解的状态管理核心思想是解耦。 和别的框架一样,Flutter中的状态管理主要是对逻辑、数据等进行解耦,以适应复杂业务的开发及维护。

2、对比其它框架

最经典的解耦就是MVC了,划分为三个模块:

  • Model层:数据层
  • View层:页面UI
  • Controller:逻辑处理
    现在很多也在用这一经典的模式,MVC模式可以解决大部分的耦合问题,但也存在 Controller过于臃肿的情况,所以后面发展而来了 MVVMMVP等模式,用于在不同的场景下更好的解耦。

3、Flutter状态管理

Flutter中的状态管理主要是对逻辑层状态层页面层行为层等进行划分:

  • view:界面层,主要是UI
  • Logic:逻辑层,主要处理业务逻辑
  • State:状态层,主要处理页面所需数据状态
  • Action:行为层,主要处理交互事件
  • Reducer:这个层级,是专门用于处理数据变化的

不同的解耦层可以组合成不同的模式,如MVC是由逻辑层、数据层、页面层组成,在Flutter中可以分为极简模式(页面层+逻辑层)、标准模式(页面层+逻辑层+状态层)、严格模式(页面层+逻辑层+状态层+行为层)、强迫症模式(页面层+逻辑层+状态层+行为层+ Reducer层),几种模式各有优缺点,下面我会结合具体的状态管理框架详细的讲下这几种模式。

二、状态管理模式

2.1、极简模式

状态管理-极简模式.png

从上图可以看出极简模式由逻辑层+页面层组成,各自的职责如图所示,非常的简洁,对于不是太复杂的业务使用与用该模式开发,如果一些简单的页面也用复杂的模式感觉有点生搬硬套了,有的分层完全是不需要的,也造成了内存的浪费。
现在较为流行的状态管理框架也是按照极简模式划分的,如providergetx,两者的区别不大,不过在选择使用时选provider可能需要能hold的住InheritedWidget,选getx可能需要能hold的住依赖注入,回收GetXController

2.2、标准模式

状态管理-标准模式.png

从上图可以看出标准模式由 逻辑层+页面层+状态层组成,各自的职责如图所示,相比极简模式,标准格式多了状态层,状态层主要是存储所需的状态变量。标准模式类似于经典的MVC的分层,用的十分普遍,能够很好的解耦。
常见的框架有cubitprovidergetx

2.3、严格模式

状态管理-严格模式.png

从上图可以看出标准模式由 逻辑层+页面层+状态层+行为层组成,各自的职责如图所示,相比标准模式,严格格式多了一个行为层,行为层从图中可以清楚的看出主要是为了处理交互事件,那为什么要多出这一层呢?其实在Flutter中使用标准模式时会存在一个问题,很多的交互事件都是深埋在各个widget中,查找起来非常不变,事件少了还好,如果后续越来越复杂事件越来越多那就成了一团麻了。有了行为性这一分层,可以很好的处理交互事件,页面有什么交互事件,交互事件如何处理只需到Action文件中查找即可,对于后期的维护来说十分方便。
常见的状态管理框架:BlocReduxfish_redux

2.4、强迫症模式

状态管理-强迫症模式.png

从上图可以看出标准模式由 逻辑层+页面层+状态层+行为层+Reducer层组成,各自的职责如图所示,相比严格模式,强迫症模式多了一个Reducer层Reducer层主要是对数据进行处理并更新后刷新页面。从上图来看这个结构已经有点复杂了,为了解耦数据刷新这一层次,付出了巨大的成本
常见的状态管理框架:Reduxfish_redux

三、状态管理框架对比

3.1、Bloc

BLoC是谷歌提出的一种设计模式,利用stream流的方式实现界面的异步渲染和重绘,我们可以非常顺利的通过BLoC实现业务与界面的分离。在使用BLoC前需要了解三个重要的概念,分别是 CubitBlocObserverBLoC

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 方法,在这个方法里,通过 EventTransformerevent转换为更新状态的方法来刷新状态数据。每个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将页面分为业务和视图两层,并定义NotifierConsumer两个核心概念:Notifier负责实现业务逻辑,且在数据更新时发出通知。Consumer负责实现界面逻辑,并在数据更新时更新自身,以及用户交互时调用Notifier方法。

优缺点分析:
优点:
  • 基于官方InheritedWidget的封装,不存在任何风险,很稳定且不会给性能方面加负担;
  • 方案涉及概念少,上手成本低;
缺点:
  • 数据流分为业务、视图两层。项目规模变大时,业务层复杂度容易指数级增长;
  • context强关联,有Flutter开发经验的都知道,context大多时候基本都是在widget中才能获取到,在其他地方想随时获取 BuildContext 是不切实际的,也就意味着大多时候只能在view层去获取到Provider提供的信息。

3.3、GetX

GetX 是 Flutter 上的一个轻量且强大的解决方案,也是我现在项目中正在使用的框架,在Flutter状态管理中绝对算是异军突起,一经发布就因其简单且全面的优势,引得一大批簇拥者。GetX可以称之为全家桶式的框架,具有以下多种功能:

  • 主题切换:比如深色模式切换;
  • 多语言:可以通过配置 Map 搞定多语言;
  • 弹窗提醒:包括了 SnackBar 和对话框;
  • 路由:无需 context 的路由跳转;
  • 离线存储:不依赖原生的key-value 存储组件的离线存储 GetStorage
  • 状态管理:快速接入的响应式状态管理;
  • 工具类:例如表单验证工具,获取系统参数(平台类型,屏幕尺寸等);
  • 依赖注入容器:使用简单的 putfind 方法完成容器对象的注册和获取;
  • 网络请求:可以使用 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,目录如下:

GetX状态管理.png

将页面和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 视图类。
fish_redux.png

在开发中可下载 FishReduxTemplate插件,可快速生成对应的文件:

FishReduxTemplateu.png

fish_redux相比其它框架多了actionreducer层,其中action层主要是行为层,统一转发处理交互事件,我认为这一层还是很有必要的,多了一层从框架端来说复杂度是提高了,但是对于使用者来说却是结构清晰了,如果没有行为层,交互事件大多都埋在各种widget中,这样找起来非常不方便,而且随着后期的迭代也会变得越来越难维护,有了行为层就清晰多了。对于reducer层我保留意见,可能是水平达不到,我没有感受到这层妙处在哪。

优缺点分析:
优点:
  • 结构清晰,适合团队开发,也利于后期维护;
缺点:
  • 更新速度较差,我们项目已放弃fish_redux,因为3.0版本还没有适配空安全;
  • 结构会较为复杂,上手难度大,学习成本较高。

[注]:选择哪种状态管理框架没有准确的答案,其受难易程度、可维护性、开发成本、性能、应用场景、团队技术栈等因素的影响,所以适合自己的才是最好的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容