27 · 状态管理选型与横向对比
本文定位:不是"再教你一遍 Riverpod"(那是 08 篇), 而是回答三个 08 篇没覆盖、但你迟早会被问到的问题: ① 为什么是 Riverpod 而不是别的?② 遇到别的方案(老代码/新项目/团队争论)怎么接?③ 要不要迁移?
⚠️ 版本警告:本项目是 Riverpod 2.6.1。文中 Riverpod 示例严格按 2.x 语法。 生态版本号(各库当前版本)易变,属于"易变信息",请以 pub.dev 为准,本文只讲判断框架。
开篇:前端概念对照
你在前端早就见过这套格局的"翻版",Flutter 的状态管理之争 ≈ 前端的 Redux/Zustand/MobX 之争。
| 前端方案 | Flutter 对应 | 心智模型 | 相似点 |
|---|---|---|---|
| Context API + useReducer | Provider | 沿组件树往下传 | 官方、简单、依赖上下文 |
| Zustand / Jotai / Pinia | Riverpod | 独立于组件树的 store | 不依赖 context、好测试 |
| Redux / Vuex | Bloc | 事件 → 状态,强约束 | 样板代码多、可追踪 |
Vue 2 的 Vue.prototype 全局挂 | GetX | 全局单例,哪都能拿 | 极简、但容易滥用 |
Solid Signals / Vue ref | signals | 细粒度自动依赖收集 | 不用手动声明依赖 |
一句话记住:Flutter 没有"唯一正确解",和前端一样——选型取决于团队规模、项目复杂度和对可追踪性的要求。
一、你什么时候会遇到选型问题
不要为了"学"而学,下面四个场景才是真实触发点:
| 场景 | 你会遇到 | 该读本文第几节 |
|---|---|---|
| 接手老 Flutter 项目,用的是 Provider | 要不要迁到 Riverpod? | 七(迁移成本) |
| 新起一个小项目/模块 | 继续 Riverpod 还是上 Bloc? | 六(决策树) |
| 团队有人说"GetX 写起来快" | 能采纳吗?风险是什么? | 四(GetX 详解) |
| 听说 Signals 性能更好 | 值得换吗? | 四(Signals)+ 七 |
二、五大方案速览(一句话定位)
| 方案 | 一句话定位 | 哲学 |
|---|---|---|
| Provider | 官方推荐的"入门款" | 把状态挂在 Widget 树上,用 InheritedWidget 传递 |
| Riverpod | Provider 的重写版(同作者) | 状态独立于 Widget 树,编译期安全 |
| Bloc | 企业级"强约束"方案 | 一切皆 Event → State,可完整追踪状态变更历史 |
| GetX | 极简"全家桶" | 一个库解决状态+路由+依赖注入+国际化 |
| Signals | 细粒度响应式新秀 | 自动依赖收集,精确到"哪个字段变了" |
三、核心差异横向对比
| 维度 | Provider | Riverpod | Bloc | GetX | Signals |
|---|---|---|---|---|---|
依赖 BuildContext | ✅ 需要 | ❌ 不需要 | ✅ 需要(写法上) | ❌ 不需要 | ❌ 不需要 |
| 编译期安全 | ❌ 运行时可能报错 | ✅ | ✅ | ❌ | ✅ |
| 样板代码量 | 少 | 中(代码生成后很少) | 多 | 极少 | 少 |
| 测试友好度 | 一般(需 mock context) | ✅ 好 | ✅ 很好 | 一般 | 好 |
| 学习曲线 | 低 | 中 | 高 | 低 | 中 |
| 状态变更可追踪 | ❌ | 一般(可加 Observer) | ✅ 完整事件流 | ❌ | 一般 |
| 细粒度更新 | 需手动 Selector | 需 select | 需 buildWhen | 自动 | ✅ 自动且最细 |
| 编译期代码生成 | 不需要 | 推荐(riverpod_generator) | 可选 | 不需要 | 不需要 |
| 生态/社区 | 大(官方) | 大且活跃 | 大(企业多) | 大但争议大 | 小(较新) |
| 官方推荐度 | 官方 | 作者推荐(Provider 继任) | 社区主流 | ❌ 官方不推荐 | 观望中 |
GetX 的争议点(客观陈述,供你自己判断): 它把路由、依赖注入、国际化、状态管理全塞进一个库,违反"单一职责";
Get.put()全局单例可随处获取,方便但极易造成隐式耦合和内存泄漏; 社区有"离开了 GetX 就不会写 Flutter"的批评(因为绕过了 Flutter 自身的 Navigator/InheritedWidget 机制)。 结论:个人小项目/快速验证可以;中大型团队项目不建议。
四、逐个方案详解
① Provider —— 老项目的"存量方案"
// 定义
class Counter extends ChangeNotifier {
int value = 0;
void increment() {
value++;
notifyListeners(); // 手动通知
}
}
// 提供(必须挂在 Widget 树上)
ChangeNotifierProvider(
create: (_) => Counter(),
child: MyApp(),
)
// 消费(依赖 context)
final counter = context.watch<Counter>();优点:官方、简单、资料多。 缺点:
- 依赖
BuildContext,在initState里直接用会报错,得用context.read - 运行时才报错:忘了在树上提供
Provider,运行时抛ProviderNotFoundException - 多个同类型状态要嵌套多层
MultiProvider
什么时候用它:维护老项目、或写很小的 demo。新项目不建议。
② Riverpod —— 本项目在用(重点)
// 定义(2.x + 代码生成)
@riverpod
class Counter extends _$Counter {
@override
int build() => 0; // 初始状态
void increment() => state++;
}
// 消费(不依赖 context,用 ref)
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}为什么 HaierEnergy 选它(结合 08 篇):
| 理由 | 说明 |
|---|---|
| 编译期安全 | 漏写 ProviderScope 直接编译报错,不会上线才炸 |
| 不依赖 context | 在 mixin、工具类、非 Widget 层都能访问(项目里 DeviceManageMixin 就是这么用的) |
| 自动 dispose | 无监听者时自动释放,配合 ref.onDispose 管理资源 |
| 测试友好 | provider 可脱离 Widget 直接单测 |
什么时候不用它:团队已经统一用 Bloc 且需要完整事件审计时。
③ Bloc —— 需要"完整事件审计"时
// 三个文件:event / state / bloc
// counter_event.dart
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// counter_bloc.dart
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
// 消费
BlocBuilder<CounterBloc, int>(
builder: (context, count) => Text('$count'),
)优点:
- 状态变更有完整事件流(
BlocObserver可记录每一次 Event / Transition)→ 出问题时能完整复现用户操作路径 - 强约束,团队协作时"写法统一",新人不容易写歪
- 与
flutter_bloc的BlocBuilder/BlocListener/BlocConsumer配合成熟
缺点:样板代码多(一个小功能要建 3 个文件),学习曲线最陡。
什么时候用它:
- 金融 / 医疗等需要审计和复现的业务
- 大团队(10+ 人)需要强约束统一写法
- 状态机复杂(如多步表单、支付流程)—— Bloc 的
Event → State天然适合
④ GetX —— 快,但有代价
class CounterController extends GetxController {
final count = 0.obs; // .obs 变成可观察
void increment() => count.value++;
}
// 消费(Obx 自动收集依赖)
Obx(() => Text('${controller.count.value}'))优点:代码量最少,.obs + Obx 心智负担低;路由/依赖注入/SnackBar 都自带。 缺点:见第三节"争议点"——隐式耦合、全局单例难测试、绕过 Flutter 原生机制。
建议:本项目(HaierEnergy,团队项目)不引入。个人练手/独立小工具随意。
⑤ Signals —— 细粒度响应式的新方向
final count = signal(0); // 信号
final doubled = computed(() => count.value * 2); // 派生,自动依赖
// 消费
Watch((context) => Text('${count.value}'))核心差异:ref.watch / context.watch 是手动声明依赖;Signals 是自动收集—— 函数体里读了谁就依赖谁,粒度精确到字段。
优点:细粒度更新天然最优(不用手动 select),心智模型接近 SolidJS/Vue 的 ref。 缺点:生态较新(2023 后兴起),可参考资料和踩坑记录少,与 Riverpod/Bloc 的互操作还在演进。
建议:关注但不急于替换。可在非核心模块小范围试用。
五、代码量对比(同一个"登录表单"功能)
用"账号+密码输入、校验、提交、loading、错误提示"这个真实功能对比,感受样板代码差异:
| 方案 | 需要的文件 | 大致行数 | 说明 |
|---|---|---|---|
| Provider | 1(ChangeNotifier 类) | ~60 行 | 手动 notifyListeners,校验逻辑混在类里 |
| Riverpod | 1(+1 生成的 .g.dart) | ~50 行 | 代码生成省掉模板,状态用 copyWith |
| Bloc | 3(event + state + bloc) | ~120 行 | 每个交互一个 Event 类 |
| GetX | 1 | ~35 行 | 最少,但依赖注入是隐式的 |
| Signals | 1 | ~45 行 | 不用声明依赖,字段级更新 |
行数只是量级参考。代码少 ≠ 好维护——Bloc 的 120 行换来的是完整事件追踪,在复杂业务里反而更省钱。
六、选型决策树(直接照着走)
开始
│
├─ 维护老项目? ── 是 ──→ 沿用原方案(别为了"先进"而重写)
│ └─ 确实要迁?看第七节迁移成本
│
├─ 需要完整事件审计/复现用户操作? ── 是 ──→ Bloc
│
├─ 团队 ≥10 人、需要强约束统一写法? ── 是 ──→ Bloc
│
├─ 中大型团队项目(如 HaierEnergy)? ── 是 ──→ Riverpod ✅
│
├─ 个人小项目/快速验证/独立工具? ── 是 ──→ GetX 或 Riverpod
│ └─ 想长期维护 → Riverpod
│
└─ 想要最细粒度自动更新、愿意吃螃蟹? ── 是 ──→ Signals(小范围试用)本项目结论:HaierEnergy 用 Riverpod 2.6.1 是合理选择—— 它是 Provider 作者的重写的继任者,编译安全、不依赖 context、测试友好, 且已经和 GoRouter 14 / Freezed 2.5 / Retrofit 4 组成了完整链路(见 07 篇、09 篇)。没有足够理由更换。
七、迁移成本矩阵
| 从 → 到 | 成本 | 关键工作 | 建议 |
|---|---|---|---|
| Provider → Riverpod | 中低 | ① 根节点包 ProviderScope ② ChangeNotifier 改 Notifier ③ context.watch 改 ref.watch ④ notifyListeners 改为给 state 赋值 | ✅ 官方有迁移指南,推荐迁 |
| Riverpod → Bloc | 高 | 要拆出 Event/State,重建整个状态层 | ⚠️ 除非有审计刚需,否则不值 |
| Bloc → Riverpod | 高 | 事件流模型要改成 Notifier 模型 | ⚠️ 同上,除非团队决心换 |
| Riverpod → Signals | 中 | 概念可映射(provider↔signal,select↔自动),但语法全改 | 🔍 新模块试用,存量不动 |
| 任意 → GetX | 低(但反向极高) | 改成 .obs 很快 | ❌ 进去容易出来难(隐式依赖散落各处) |
迁移的通用原则
- 增量迁移,不要 Big Bang:Riverpod 和 Provider 可以共存,新模块用新方案,老模块逐步改。
- 先迁"叶子":先改不依赖其他状态的独立模块,再往上改。
- 迁移前先补测试(见 19 篇)——没有测试的迁移等于裸奔。
- 不要为了迁移而迁移:老方案能跑、没人抱怨、没有痛点,就别动。
八、动手练习
用同一个需求写两遍,体会差异。需求:一个计数器,显示"当前值"和"是否超过 5"两个文本。
练习 A:用 Riverpod(本项目写法)
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
// 两个 Widget 分别 watch,体会 select 精准订阅
final value = ref.watch(counterProvider);
final isOver5 = ref.watch(counterProvider.select((v) => v > 5));练习 B:用 Signals 写同样的功能
final count = signal(0);
final isOver5 = computed(() => count.value > 5);
// 注意:这里不需要手写 select对比观察点:
select是手动声明依赖(Riverpod),computed是自动收集(Signals)- 把"是否超过 5"的判断改成别的字段时,哪边改动更小?
练习场:见专题 README 的「配套练习场」(
lib/main_playground.dart)。
九、自检清单
学完本文,你应该能够:
- [ ] 说出 Provider / Riverpod / Bloc / GetX / Signals 各自的一句话定位和核心哲学
- [ ] 解释 Riverpod 相比 Provider 的两个关键优势(不依赖 context、编译期安全)
- [ ] 说明 Bloc 的样板代码"贵"在哪里、换来什么(完整事件流与可追踪性)
- [ ] 客观陈述 GetX 的争议点,而不是简单说"它不好"
- [ ] 面对一个新项目,能照着第六节决策树给出选型建议并说明理由
- [ ] 评估 Provider → Riverpod 的迁移工作量和步骤
- [ ] 说出"本项目为什么是 Riverpod",以及什么情况下才值得换
十、常见误区
| 误区 | 正解 |
|---|---|
| "Riverpod 3.x 教程能直接用" | ❌ 本项目是 2.6.1,3.x 语法(如 Notifier 泛型参数)会编译失败,见 08 篇 |
| "状态管理越新越好" | ❌ 稳定、团队熟悉、生态成熟往往更重要。Signals 再好,2026 年也不该在核心项目上全量替换 Riverpod |
| "代码越少越好" | ❌ 样板代码换来的是约束和可追踪性,要看项目规模和协作人数 |
| "GetX 写起来快就用它" | ⚠️ 快是真的,但要意识到反向迁移成本极高 |
| "一个项目只能用一个方案" | ❌ 存量共存很正常(如老模块 Provider + 新模块 Riverpod),关键是边界清晰 |
相关文档
- 08 · 状态管理 Riverpod —— 本项目 Riverpod 2.6.1 的详细用法
- 07 · 项目架构总览 —— 状态层在整个架构中的位置
- 09 · 数据层链路 —— 网络数据如何进入状态层
- 19 · 测试实战 —— 迁移前如何补测试
- A07 · 状态管理演进与选型 —— 前端视角:Redux→MobX→Zustand/Jotai,Signals 是跨框架收敛方向
- 速查 06 · Riverpod —— 日常写法与常见报错速查