JHHK

欢迎来到我的个人网站
行者常至 为者常成

状态管理对比

目录

介绍

一、为什么需要这篇对比

前面几篇分别介绍了 setState / InheritedWidget、Provider、Riverpod、BLoC、GetX。
本篇把它们放到同一张桌子上,从原理、优缺点、适用规模三个维度横向对比,最后给出当前主流方案与选型结论。

二、评估维度

① 原理机制      :状态存在哪、怎么通知、怎么重建
② 样板代码      :完成一个计数器/一个网络请求页面要写多少代码
③ 学习成本      :概念数量、心智负担
④ 解耦程度      :UI 与业务逻辑是否分离、是否依赖 BuildContext
⑤ 可测试性      :能否脱离 Flutter 环境单元测试
⑥ 可维护性/约束  :是否强制架构规范(大团队协作关键)
⑦ 性能          :重建粒度能否精细控制

各方案原理与优缺点

一、setState(官方内置)

原理
Widget 内部维护 State,调用 setState() 标记当前 Element 为 dirty,下一帧统一 rebuild 该子树。

setState() → markNeedsBuild() → 加入 Dirty List → 下一帧 build()

优点
零依赖、零学习成本,官方原生。
局部状态(输入框、动画进度)最简单直接。

缺点
状态无法跨 Widget/跨页面共享。
状态与 UI 耦合在同一个 State 类里,无法单独测试。
容易滥用:把页面级数据都堆进一个 State,build 越来越臃肿。

定位:Widget 私有的局部状态,不需要共享时的默认选择。

二、InheritedWidget(官方底层机制)

原理
沿 Widget 树自上而下共享数据的官方机制。子树通过 dependOnInheritedWidgetOfExactType 注册依赖,数据变化时 notifyClients() 精确通知依赖者重建。

数据变化 → updateShouldNotify() → notifyClients() → dependents.markNeedsBuild()

优点
官方机制,所有三方方案的地基(Provider/Riverpod 底层都是它)。
依赖注册是精确的:只有真正读取数据的子树才重建。

缺点
只解决「共享」,不解决「修改」:数据变化后需要手动重建整个 InheritedWidget 才能触发通知。
样板代码多,状态管理要自己拼装。

定位:不直接用于业务,是理解其它方案原理的必修课。

三、Provider(官方推荐多年)

原理
InheritedWidget + ChangeNotifier 的封装。Notifier 持有状态,notifyListeners() 通知;UI 通过 context.watch/read 获取。

ChangeNotifier(状态+修改逻辑)
        +
InheritedWidget(依赖注册+通知)
        =
Provider

优点
官方出品,社区资料最多。
ChangeNotifier 模型简单:一个类管状态和修改方法。
Consumer / Selector 重建粒度可控。

缺点
依赖 BuildContext:脱离 Widget 树拿不到状态,深层调用要层层传 context。
运行时按类型查找:类型写错、Provider 挂错位置都是运行时才爆。
ChangeNotifier 需要手动 dispose,忘记释放会泄漏。
大型项目组合依赖(A 状态依赖 B 状态)写起来别扭。

定位:中小型项目、从 setState 迁移的第一步。

四、BLoC(事件驱动流)

原理
基于 Stream。UI 只发 Event,Bloc 内部处理业务逻辑后 emit 新 State,UI 根据 State 渲染,数据单向流动。

Event → Bloc(mapEventToState) → Stream<State> → UI

优点
架构约束最强:Event/State 强制分离,业务逻辑完全脱离 UI。
所有状态变化皆可追溯(哪个事件 → 哪个状态),配合 BlocObserver 审计。
异步处理天然优雅(Stream 原生)。
纯 Dart 实现,可测试性最好。

缺点
样板代码最多:一个功能要写 Event 类 + State 类 + Bloc 三件套。
学习成本高(EventTransformer、emit 时机等概念)。
简单页面用它是杀鸡用牛刀。

定位:大型项目、强规范团队(金融、电商中台类)。

五、GetX(全能框架)

原理
自研响应式:变量 .obs 包装成 Rx,setter 触发通知;Obx 在 build 时收集用到的 Rx 建立订阅,只重建自己。外加路由、DI、工具全家桶。

count.value++ → Rx setter → 通知 → Obx 重建

优点
开发效率极高:.obs + Obx 两步完成响应式。
不依赖 BuildContext:导航、弹窗、Toast、find 全部无 context。
全家桶:路由 + DI + 国际化 + 网络请求 + 存储一站式。
性能优秀:Obx 细粒度 + GetBuilder 零开销双方案。

缺点
架构约束最弱:怎么写全靠自觉,大项目容易失控。
全家桶与框架耦合过深:用了它的路由就很难换官方方案。
魔法多(Rx 隐式订阅、smartManagement 自动回收),出问题排查成本高。
非官方维护,大型重构风险由社区承担。

定位:个人项目、快速原型、小团队效率优先。

六、Riverpod(Provider 作者的下一代作品)

原理
状态不再挂在 Widget 树上,而是存放在 ProviderScope 容器中(仍是 InheritedWidget 分发,但状态本体在树外)。Provider 是全局声明的状态描述,通过 ref 句柄读写监听。

ProviderScope(容器,树外存状态)
        │
Provider(声明式状态定义)
        │
ref.watch/read/listen(唯一操作句柄)

优点
编译期安全:Provider 是全局常量,引用错了编译不过(对比 Provider 的运行时按类型查找)。
不依赖 BuildContext,逻辑与 UI 彻底分离。
组合能力强:Provider 之间可以互相 watch,天然处理依赖关系。
autoDispose + family:生命周期与参数化开箱即用。
AsyncValue 统一处理 loading/error/data 三态。
可测试性好。

缺点
学习成本最高:概念多(各类 Provider、ref、修饰符、代码生成)。
简单页面样板代码显得繁琐。
强依赖作者个人生态,API 迭代快(1.x → 2.x → 3.x 迁移成本)。

定位:中大型项目的现代首选。

横向对比

一、总对比表

对比项 Provider BLoC GetX Riverpod
原理 InheritedWidget + ChangeNotifier Stream 事件驱动 Rx 响应式 + 全家桶 树外容器 + Provider 声明
状态存放 Widget 树上 Bloc 实例(树外) 全局注册表 ProviderScope 容器
样板代码 少 多 极少 中
学习成本 低 高 低 高
context 依赖 需要 需要 不需要 不需要
编译期安全 弱(运行时查类型) 强 弱(find 运行时) 强(编译期检查)
架构约束 弱 强(Event/State 分离) 极弱 中(推荐 Notifier 模式)
可测试性 中 最好 中 好
异步支持 手动处理 原生 Stream 手动/GetConnect AsyncValue 三态
自动释放 手动 dispose BlocProvider 自动 smartManagement autoDispose
适合规模 小~中 中~大 小~中 中~大

二、重建粒度对比

方案 局部刷新手段
Provider 拆 Widget / Consumer / Selector / const
BLoC 拆 Widget / BlocBuilder(buildWhen) / BlocSelector
GetX Obx 最小包裹 / GetBuilder / update([id])
Riverpod 拆 ConsumerWidget / Consumer / select

结论:四者都能做到细粒度刷新,真正决定性能的不是框架,而是「监听范围放得多低」——这一点上它们殊途同归。

三、读写 API 对照

语义 Provider BLoC GetX Riverpod
读数据并监听 context.watch BlocBuilder Obx + .value ref.watch
只取实例不监听 context.read context.read Get.find ref.read
变化执行副作用 BlocListener ever / debounce ref.listen  

主流方案与选型建议

一、目前主流格局

第一梯队(新项目首选)

① Riverpod
原因:官方文档已将其与 Provider 并列为推荐方案;编译期安全 + 树外状态 + 组合能力,正面解决了 Provider 的所有历史痛点;AsyncValue 对网络请求三态的封装已成为社区范式。

② BLoC
原因:大型企业项目的事实标准(尤其金融、电商);Event/State 的强约束让百人团队代码风格强制统一;可追溯性和可测试性无可替代。

第二梯队(存量巨大)

③ Provider
原因:存量代码最多、资料最全;但作者本人已转向 Riverpod,新特性(如代码生成)不再投入,属于「稳定但不演进」状态。

第三梯队(特定场景)

④ GetX:个人项目、快速验证依然高效,但架构约束弱 + 与官方 API 渐行渐远,团队项目慎选。
⑤ Signals / MobX:响应式新势力,值得关注但生态未定型。

二、选型决策树

项目是什么规模?
   │
   ├── 个人/ Demo / 快速原型
   │        │
   │        ▼
   │   GetX 或 setState(效率优先)
   │
   ├── 中小型项目(几人维护)
   │        │
   │        ▼
   │   Provider(简单熟悉)或 Riverpod(面向未来)
   │
   └── 中大型项目(团队协作、长期演进)
            │
            ├── 团队需要强规范、业务流程复杂 → BLoC
            │
            └── 追求现代语法、编译期安全    → Riverpod

三、选型的本质

技术选型不是选「最强的」,而是选「最合适的」:

① 小项目选 GetX:省下的样板代码就是省下的时间。
② 大团队选 BLoC:约束带来的可维护性,远超样板代码的成本。
③ 长期项目选 Riverpod:编译期安全和组合能力,让项目越写越稳而不是越写越乱。
④ 所有项目都别忘了 setState:局部状态永远不需要第三方框架。

状态管理方案没有银弹,理解它们的共同内核(状态存放 + 变化通知 + 精确重建),比记住任何一个 API 都重要。


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.