目录
介绍
一、为什么需要这篇对比
前面几篇分别介绍了 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 都重要。
行者常至,为者常成!