Skip to content

27 · 状态管理选型与横向对比 ​

本文定位:不是"再教你一遍 Riverpod"(那是 08 篇), 而是回答三个 08 篇没覆盖、但你迟早会被问到的问题: ① 为什么是 Riverpod 而不是别的?② 遇到别的方案(老代码/新项目/团队争论)怎么接?③ 要不要迁移?

⚠️ 版本警告:本项目是 Riverpod 2.6.1。文中 Riverpod 示例严格按 2.x 语法。 生态版本号(各库当前版本)易变,属于"易变信息",请以 pub.dev 为准,本文只讲判断框架。


开篇:前端概念对照 ​

你在前端早就见过这套格局的"翻版",Flutter 的状态管理之争 ≈ 前端的 Redux/Zustand/MobX 之争。

前端方案Flutter 对应心智模型相似点
Context API + useReducerProvider沿组件树往下传官方、简单、依赖上下文
Zustand / Jotai / PiniaRiverpod独立于组件树的 store不依赖 context、好测试
Redux / VuexBloc事件 → 状态,强约束样板代码多、可追踪
Vue 2 的 Vue.prototype 全局挂GetX全局单例,哪都能拿极简、但容易滥用
Solid Signals / Vue refsignals细粒度自动依赖收集不用手动声明依赖

一句话记住:Flutter 没有"唯一正确解",和前端一样——选型取决于团队规模、项目复杂度和对可追踪性的要求。


一、你什么时候会遇到选型问题 ​

不要为了"学"而学,下面四个场景才是真实触发点:

场景你会遇到该读本文第几节
接手老 Flutter 项目,用的是 Provider要不要迁到 Riverpod?七(迁移成本)
新起一个小项目/模块继续 Riverpod 还是上 Bloc?六(决策树)
团队有人说"GetX 写起来快"能采纳吗?风险是什么?四(GetX 详解)
听说 Signals 性能更好值得换吗?四(Signals)+ 七

二、五大方案速览(一句话定位) ​

方案一句话定位哲学
Provider官方推荐的"入门款"把状态挂在 Widget 树上,用 InheritedWidget 传递
RiverpodProvider 的重写版(同作者)状态独立于 Widget 树,编译期安全
Bloc企业级"强约束"方案一切皆 Event → State,可完整追踪状态变更历史
GetX极简"全家桶"一个库解决状态+路由+依赖注入+国际化
Signals细粒度响应式新秀自动依赖收集,精确到"哪个字段变了"

三、核心差异横向对比 ​

维度ProviderRiverpodBlocGetXSignals
依赖 BuildContext✅ 需要❌ 不需要✅ 需要(写法上)❌ 不需要❌ 不需要
编译期安全❌ 运行时可能报错✅✅❌✅
样板代码量少中(代码生成后很少)多极少少
测试友好度一般(需 mock context)✅ 好✅ 很好一般好
学习曲线低中高低中
状态变更可追踪❌一般(可加 Observer)✅ 完整事件流❌一般
细粒度更新需手动 Selector需 select需 buildWhen自动✅ 自动且最细
编译期代码生成不需要推荐(riverpod_generator)可选不需要不需要
生态/社区大(官方)大且活跃大(企业多)大但争议大小(较新)
官方推荐度官方作者推荐(Provider 继任)社区主流❌ 官方不推荐观望中

GetX 的争议点(客观陈述,供你自己判断): 它把路由、依赖注入、国际化、状态管理全塞进一个库,违反"单一职责"; Get.put() 全局单例可随处获取,方便但极易造成隐式耦合和内存泄漏; 社区有"离开了 GetX 就不会写 Flutter"的批评(因为绕过了 Flutter 自身的 Navigator/InheritedWidget 机制)。 结论:个人小项目/快速验证可以;中大型团队项目不建议。


四、逐个方案详解 ​

① Provider —— 老项目的"存量方案" ​

dart
// 定义
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 —— 本项目在用(重点) ​

dart
// 定义(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 —— 需要"完整事件审计"时 ​

dart
// 三个文件: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 —— 快,但有代价 ​

dart
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 —— 细粒度响应式的新方向 ​

dart
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、错误提示"这个真实功能对比,感受样板代码差异:

方案需要的文件大致行数说明
Provider1(ChangeNotifier 类)~60 行手动 notifyListeners,校验逻辑混在类里
Riverpod1(+1 生成的 .g.dart)~50 行代码生成省掉模板,状态用 copyWith
Bloc3(event + state + bloc)~120 行每个交互一个 Event 类
GetX1~35 行最少,但依赖注入是隐式的
Signals1~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 很快❌ 进去容易出来难(隐式依赖散落各处)

迁移的通用原则 ​

  1. 增量迁移,不要 Big Bang:Riverpod 和 Provider 可以共存,新模块用新方案,老模块逐步改。
  2. 先迁"叶子":先改不依赖其他状态的独立模块,再往上改。
  3. 迁移前先补测试(见 19 篇)——没有测试的迁移等于裸奔。
  4. 不要为了迁移而迁移:老方案能跑、没人抱怨、没有痛点,就别动。

八、动手练习 ​

用同一个需求写两遍,体会差异。需求:一个计数器,显示"当前值"和"是否超过 5"两个文本。

练习 A:用 Riverpod(本项目写法) ​

dart
@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 写同样的功能 ​

dart
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),关键是边界清晰

相关文档 ​