本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
这是 《Flutter 工程化框架选择》 系列的第六篇 ,就像之前说的,这个系列只是单纯告诉你,创建一个 Flutter 工程,或者说搭建一个 Flutter 工程脚手架,应该如何快速选择适合自己的功能模块,或者说这是一个指引系列,所以比较适合新手同学。
其实这是我最不想写的一个篇。
状态管理是 Flutter 里 ♾️ 的话题,本质上 Flutter 里的状态管理就是传递状态和基于 setState
的封装,状态管理框架解决的是如何更优雅地共享状态和调用 setState
。
那为什么我不是很想写状态管理的对比内容?
首先因为它很繁,繁体的煩,从 Flutter 发布到现在,scoped_model
、BLoC
、Provider
、 flutter_redux
、MobX
、 fish_redux
、Riverpod
、GetX
等各类框架“百花齐放”,虽然这对于社区来说是这是好事,但是对于普通开发者来说很容易造成过度选择困难症,特别早期不少人被各种框架“伤害过”。
其次,状体管理在 Flutter 里一直是一个“敏感”话题,每次聊到状态管理就绕不开 GetX
,但是一旦聊 GetX
又会变成“立场”问题,所以一直以来我都不是很喜欢写状态管理的内容。
所以本来应该在第一篇就出现的内容,一直被拖到现在才放出来,这里提前声明一些,本篇不会像之前一样从大小和性能等方面去做对比,因为对于状态管理框架来说这没什么意义:
- 集成后对大小的影响可能还不如一张图片
- 性能主要取决于开发者的习惯,在状态管理框架上对比性能其实很主观
当然,如果你对集成后对大小的影响真的很在意,那可以在打包时通过 --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
就可以进行分析。
例如这里是将 Riverpod
和 GetX
在同一项项目集成后导出不同 json 在 Diff 进行对比,可以看到此时差异也就在 78.5kb ,这个差异大小还不如一张 png 资源图片的影响大。
所以本次主要是从这些状体管理框架自身的特点出发,简单列举它们的优劣,至于最后你觉得哪个适合你,那就见仁见智了~
本篇只是告诉你它们的特点和如何去选择,并不会深入详细讲解,如果对实现感兴趣的可以看以前分享过的文章:
2019 年的 Google I/O 大会 Provider 成了 Flutter 官方新推荐的状态管理方式之一,它的特点就是: 不复杂,好理解,代码量不大的情况下,可以方便组合和控制刷新颗粒度 , 其实一开始官方也有一个 flutter-provide ,不过后来宣告GG , Provider 成了它的替代品。
⚠️ 注意,provider
比flutter-provide
多了个r
,所以不要再看着 provide 说 Provider 被弃坑了。
简单来说,Provider 就是针对 InheritedWidget
的一个包装工具,他让 InheritedWidget
的使用变得更简单,在往下共享状态的同时,可以通过 ChangeNotifier
、 Stream
、Future
配合 Consumer*
组合出多样的更新模式。
所以使用 Provider 的好处之一就是简单,同时你可以通过 Consumer*
等来决定刷新的颗粒度,其实也就是 BuildContext
在 of(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 和 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 算是 Flutter 早期比较知名的状态管理框架,它同样是存在 bloc
和 flutter_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 虽然也是 pub 上的 Flutter Favorite 的项目,但是现在的 Flutter 开发者应该都不怎么使用它,而恰好我在刚使用 Flutter 时使用的状态管理框架就是它。
其实前端开始者对 redux 可能会更熟悉一些,当时我恰好用 RN 项目切换到 Flutter 项目,在 RN 时代我就一直在使用 redux,flutter_redux 自然就成了我首选的状态管理框架。
其实这也是 Flutter 最有意思的,很多前端的状态管理框架都可以迁移到 Flutter ,例如 flutter_redux 里就是利用了 Stream
特性,通过 redux
单向事件流的设计模式来完成解耦和拓展。
在 flutter_redux 中,开发者的每个操作都只是一个 Action
,而这个行为所触发的逻辑完全由 middleware
和 reducer
决定,这样的设计在一定程度上将业务与UI隔离,同时也统一了状态的管理。
当然缺陷也很明显,你要写一堆代码,开发逻辑一定程度上也不大符合 Flutter 的开发习惯。
优点:
- 解耦
- 对 redux 开发友好
- 适合中大型项目里协作开发
缺点:
- 影响开发速度,要写一堆模版
- 不是很贴合 Flutter 开发思路
说到 redux 就不得不说 fish_redux ,如果说 redux 是搭积木,那闲鱼最早开源的 fish_redux 可以说是积木界的乐高,闲鱼在 redux
的基础上提出了 Comoponent
的概念,这个概念下 fish_redux
是从 Context
、Widget
等地方就开始全面“入侵”你的代码,从而带来“超级赛亚人”版的 redux
。
所以不管是 flutter_redux 还是 fish_redux 都是很适合团队协作的开发框架,但是它的开发体验和开发过程,注定不是很友好。
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.put
、Get.find
、Get.to
等操作完全无需 Widget 介入- 内置的
extension
如各类基础类似的*.obs
通过GetStream
实现了如var count = 0.obs;
和Obx(() => Text("${controller.name}"));
这样的简化绑定操作
那 GetX 是如何脱离 Context 的依赖?说起来也不复杂,例如 :
-
GetMaterialApp
内通过一个会有一个GlobalKey
用于配置MaterialApp
的navigatorKey
,这样就可以通过全局的navigatorKey
获取到Navigator
的State
,从而调用push
API 打开路由 -
Get.put
和Get.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 工程选择上还有哪些茫然,欢迎留言评论。