Flutter 工程化框架选择 — 状态管理何去何从

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

这是 《Flutter 工程化框架选择》 系列的第六篇 ,就像之前说的,这个系列只是单纯告诉你,创建一个 Flutter 工程,或者说搭建一个 Flutter 工程脚手架,应该如何快速选择适合自己的功能模块,或者说这是一个指引系列,所以比较适合新手同学。

其实这是我最不想写的一个篇

状态管理是 Flutter 里 ♾️ 的话题,本质上 Flutter 里的状态管理就是传递状态和基于 setState 的封装,状态管理框架解决的是如何更优雅地共享状态和调用 setState

那为什么我不是很想写状态管理的对比内容

首先因为它很繁,繁体的煩,从 Flutter 发布到现在,scoped_modelBLoCProviderflutter_reduxMobXfish_reduxRiverpodGetX 等各类框架“百花齐放”,虽然这对于社区来说是这是好事,但是对于普通开发者来说很容易造成过度选择困难症,特别早期不少人被各种框架“伤害过”。

其次,状体管理在 Flutter 里一直是一个“敏感”话题,每次聊到状态管理就绕不开 GetX ,但是一旦聊 GetX 又会变成“立场”问题,所以一直以来我都不是很喜欢写状态管理的内容。

image-20221109095950845

所以本来应该在第一篇就出现的内容,一直被拖到现在才放出来,这里提前声明一些,本篇不会像之前一样从大小和性能等方面去做对比,因为对于状态管理框架来说这没什么意义:

  • 集成后对大小的影响可能还不如一张图片
  • 性能主要取决于开发者的习惯,在状态管理框架上对比性能其实很主观

当然,如果你对集成后对大小的影响真的很在意,那可以在打包时通过 --analyze-size 来生成 analysis.json 文件用于对比分析:

flutter build apk --target-platform android-arm64 --analyze-size

上诉命令在执行之后,会在 /Users/你的用户名/.flutter-devtools/ 目录下生成一个 apk-code-size-analysis_01.json 文件,之后我们只需要打开 Flutter 的 DevTools 下的 App Size Tooling 就可以进行分析。

例如这里是将 RiverpodGetX 在同一项项目集成后导出不同 json 在 Diff 进行对比,可以看到此时差异也就在 78.5kb ,这个差异大小还不如一张 png 资源图片的影响大。

所以本次主要是从这些状体管理框架自身的特点出发,简单列举它们的优劣,至于最后你觉得哪个适合你,那就见仁见智了

本篇只是告诉你它们的特点和如何去选择,并不会深入详细讲解,如果对实现感兴趣的可以看以前分享过的文章:

Provider

2019 年的 Google I/O 大会 Provider 成了 Flutter 官方新推荐的状态管理方式之一,它的特点就是: 不复杂,好理解,代码量不大的情况下,可以方便组合和控制刷新颗粒度 , 其实一开始官方也有一个 flutter-provide ,不过后来宣告GG , Provider 成了它的替代品。

⚠️注意,providerflutter-provide 多了个 r,所以不要再看着 provide 说 Provider 被弃坑了。

简单来说,Provider 就是针对 InheritedWidget 的一个包装工具,他让 InheritedWidget 的使用变得更简单,在往下共享状态的同时,可以通过 ChangeNotifierStreamFuture 配合 Consumer* 组合出多样的更新模式。

所以使用 Provider 的好处之一就是简单,同时你可以通过 Consumer* 等来决定刷新的颗粒度,其实也就是 BuildContextof(context) 时的颗粒度控制。

登记到 InheritedWidget 里的 context 决定了更新是 rebuild 哪个ComponentElement ,感兴趣的可以看 全面理解 State 与 Provider

当然,虽然一直说 Provider 简单,但是其实还是有一些稍微“复杂”的地方,例如 select

Provider 里 select 是对 BuildContext 做了 “二次登记” 的行为,就是以前你用 context 是 watch 的时候 ,是直接把这个 Widget 登记到 Element 里,有更新就通知。

但是 select 做了二次处理,就是用 dependOnInheritedElement 做了颗粒化的判断,如果是不等于了才更新,所以它对 context 有要求,如下图对就是对 context 类型进行了判断。

所以 select 算是 Provider 里的“小魔法“之一,总的来说 Provider 是一个符合 Flutter 的行为习惯,但是不大符合前端和原生的开发习惯的优秀状态管理框架

优点:

  • 简单好维护
  • read、watch、select 提供更简洁的颗粒度管理
  • 官方推荐

缺点:

  • 相对依赖 Flutter 和 Widget
  • 需要依赖 Context

最后顺带辟个谣,之前有 “传闻” Provider 要被弃坑的说法,作者针对这个也有相应对澄清,所以你还是可以继续安心使用 Provider。

Riverpod

Riverpod 和 Provider 是同个作者,因为 Provider 存在某些局限性,所以作者根据 Provider 这个单词重新排列组合成 Riverpod

如果说 Provider 是 InheritedWidget 的封装,那 Riverpod 就是在 Provider 的基础上重构出更灵活的操作能力,最直观的就是 Riverpod 中的 Provider 可以随意写成全局,并且不依赖 BuildContext 来编写我们需要的业务逻

注意: Riverpod 中的 Provider 和前面的 Provider 没有关系。

在 Riverpod 里基本是每一个 “Provider” 都会有一个自己的 “Element” ,然后通过 WidgetRef 去 Hook 后成为 BuildContext 的替代,所以这就是 Riverpod 不依赖 Context 的 “魔法” 之一

⚠️这里的 “Element” 不是 Flutter 概念里三棵树的 Element,它是 Riverpod 里 Ref 对象的子类Ref 主要提供 Riverpod 内的 “Provider” 之间交互的接口,并且提供一些抽象的生命周期方法,所以它是 Riverpod 里的独有的 “Element” 单位。

另外对比 Provider ,Riverpod 不需要依赖 Flutter ,所以也不需要依赖 Widget,也就是不依赖 BuildContext,所以可以支持全局变量定义 “Provider” 对象。

优点:

  • 在 Provider 的基础上更加灵活的实现,
  • 不依赖 BuildContext ,所以业务逻辑也无需注入 BuildContext
  • Riverpod 会尽可能通过编译时安全来解决存在运行时异常问题
  • 支持全局定义
  • ProviderReference 能更好解决嵌套代码

缺点:

  • 实现更加复杂
  • 学习成本提高

目前从我个人角度看,我觉得 Riverpod 时当前之下状态管理的最佳选择,它灵活且专注,体验上也更符合 Flutter 的开发习惯

注意,很多人一开始只依赖 riverpod 然后发现一些封装对象不存在,因为 riverpod 是不依赖 flutter 的实现,所以在 flutter 里使用时不要忘记要依赖 flutter_riverpod

BLoC

BLoC 算是 Flutter 早期比较知名的状态管理框架,它同样是存在 blocflutter_bloc 这样的依赖关系,它是基于事件驱动来实现的状态管理

flutter_bloc 基于事件驱动的核心就是 Stream 和 Provider , 是的, flutter_bloc 依赖于 Provider,然后在其基础上设计了基于 Stream 的事件响应机制。

所以严格意义上 BLoC 其实是 Provider + Stream ,如果你一直很习惯基于事件流开发模式,那么 BLoC 就很适合你,但是其实从我个人体验上看,BLoC 在开发节奏上并不是快,相反还有点麻烦,不过优势也很明显,基于 Stream 的封装可以更方便做一些事件状态的监听和转换。

BlocSelector<BlocA, BlocAState, SelectedState>(
  selector: (state) {
    // return selected state based on the provided state.
  },
  builder: (context, state) {
    // return widget here based on the selected state.
  },
)

MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocB, BlocBState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
    ),
  ],
  child: ChildA(),
)

优点:

  • 代码更加解耦,这是事件驱动的特性
  • 把状态更新和事件绑定,可以灵活得实现状态拦截,重试甚至撤回

缺点:

  • 需要写更多的代码,开发节奏会有点影响
  • 接收代码的新维护人员,缺乏有效文档时容易陷入对着事件和业务蒙圈
  • 项目后期事件容易混乱交织

类似的库还有 rx_bloc ,同样是基于 Stream 和 Provider , 不过它采用了 rxdart 的 Stream 封装。

flutter_redux

flutter_redux 虽然也是 pub 上的 Flutter Favorite 的项目,但是现在的 Flutter 开发者应该都不怎么使用它,而恰好我在刚使用 Flutter 时使用的状态管理框架就是它。

其实前端开始者对 redux 可能会更熟悉一些,当时我恰好用 RN 项目切换到 Flutter 项目,在 RN 时代我就一直在使用 redux,flutter_redux 自然就成了我首选的状态管理框架。

其实这也是 Flutter 最有意思的,很多前端的状态管理框架都可以迁移到 Flutter ,例如 flutter_redux 里就是利用了 Stream特性,通过 redux 单向事件流的设计模式来完成解耦和拓展。

在 flutter_redux 中,开发者的每个操作都只是一个 Action ,而这个行为所触发的逻辑完全由 middlewarereducer 决定,这样的设计在一定程度上将业务与UI隔离,同时也统一了状态的管理。

当然缺陷也很明显,你要写一堆代码,开发逻辑一定程度上也不大符合 Flutter 的开发习惯。

优点:

  • 解耦
  • 对 redux 开发友好
  • 适合中大型项目里协作开发

缺点:

  • 影响开发速度,要写一堆模版
  • 不是很贴合 Flutter 开发思路

说到 redux 就不得不说 fish_redux ,如果说 redux 是搭积木,那闲鱼最早开源的 fish_redux 可以说是积木界的乐高,闲鱼在 redux 的基础上提出了 Comoponent 的概念,这个概念下 fish_redux 是从 ContextWidget 等地方就开始全面“入侵”你的代码,从而带来“超级赛亚人”版的 redux

所以不管是 flutter_redux 还是 fish_redux 都是很适合团队协作的开发框架,但是它的开发体验和开发过程,注定不是很友好

GetX

GetX 可以说是 Flutter 界内大名鼎鼎,Flutter 不能没有 GetX 就像程序员不能没有 PHP ,GetX 很好用,很具备话题,很全面同时也很 GetX

严格意义上说现在 GetX 已经不是一个简单的状态管理框架,它是一个统一的 Flutter 开发脚手架,在 GetX 内你可以找到:

  • 状态管理
  • 路由管理
  • 多语言支持
  • 页面托管
  • Http GetConnect
  • Rx GetStream
  • 各式各样的 extension

可以说大部分你想到的 GetX 里都有,甚至还有基于 GetX 的 get_storage 实现纯 Dart 文件级 key-value 存储支持。

所以很多时候使用 GetX 开发甚至不需要关心 Flutter ,当然这也导致经常遇到的奇怪情况:大家的问题集中在 GetX 里如何 xxxx,而不是 Flutter 如何 xxxx所以 GetX 更像是依附在 Flutter 上的解决方案

当然,使用 GetX 最直观的就是不需要 BuildContext ,甚至是你在路由跳转时都不需要关心 Context ,这就让你的代码看起来很“干净”,把整个开发过程做到“面向 GetX 开发”的效果

另外 GetX 和 Provider 等相比还具备的特色是:

  • Get.putGet.findGet.to 等操作完全无需 Widget 介入
  • 内置的 extension 如各类基础类似的 *.obs 通过 GetStream 实现了如 var count = 0.obs;Obx(() => Text("${controller.name}")); 这样的简化绑定操作

那 GetX 是如何脱离 Context 的依赖?说起来也不复杂,例如 :

  • GetMaterialApp 内通过一个会有一个 GlobalKey 用于配置 MaterialAppnavigatorKey ,这样就可以通过全局的 navigatorKey 获取到 NavigatorState ,从而调用 push API 打开路由

  • Get.putGet.find 是通过一个内部全局的静态 Map 来管理,所以在传递和存放时就脱离了 InheritedWidget ,结合 Obx ,在对获取到的 GetxController 的 value 时会有个 addListener 的操作,从而实现 Stream 的绑定和更新

可以说 GetX 内部有很多“魔法”,这些魔法或者是对 Flutter API 的 Hook、或者是直接脱离 Flutter 设计的自定义实现,总的来说 GetX “有自己的想法”

这也就带来一个了个问题,很多人新手一上手就是 GetX ,然后对 Flutter 一知半解,特别是深度解绑了 Context 之后,很多 Flutter 问题就变成了 GetX 上如何 xxxx,例如前面的: Flutter GetX 如何调用谷歌地图这种问题

如果使用 GetX 而不去思考和理解 GetX 的实现,就很容易在 Flutter 的路上走歪,比如上面各种很基础的问题。

这其实也是 GetX 的最大问题:GetX 做的很多,它入侵到很多领域,而且它拥有很多“魔法”,这些“魔法”让 Flutter 开发者不知布局的脱离了本来应有的轨迹。

当然,你说我就是想完成需求,好用就行,何必关心它们的实现呢?从这个角度看 GetX 无疑是非常不错的选择,只要 GetX 能继续维护下去并把“魔法”继续兼容。

大概就是:GetX “王国” 对初级开发者友好,但是“魔法全家桶”其实对社区的健康发展很致命

优点:

  • 瑞士军刀式护航
  • 对新人友好
  • 可以减少很多代码

缺点:

  • 全家桶,做的太多对于一些使用者来说是致命缺点,需要解决的 Bug 也多
  • “魔法”使用较多,脱离 Flutter 原本轨迹
  • 入侵性极强

总的来说,GetX 很优秀,他帮你都写好了很多东西,省去了开发者还要考虑如何去组合和思考的过程,从我个人的角度我不喜欢这种风格,但是它总归是可以帮助你提高开发效率。

另外还有一个状态管理库 Mobx ,它库采用了和 GetX 类似的风格,虽然 Mobx 的知名度和关注度不像 GetX 那么高,但是它同样采用了隐式依赖的模式,某种意义上可以把 Mobx 看成是只有状态管理版本的 GetX

最后

通过上面分享的内容,相信大家对于选哪个状态管理框架应该有自己的理解了,还是那句废话,采用什么方案和框架具体还是取决于你的需求场景,不管是哪个框架目前都有坑和局限,重点还是在于它未来是否持续维护,或者不维护了你自己能否继续维护下去

最后,如果你还有什么疑问,或者针对 Flutter 工程选择上还有哪些茫然,欢迎留言评论。

results matching ""

    No results matching ""