Flutter 状态管理 Provider 和 Riverpod 选型
为什么需要状态管理?
在 Flutter 中,Widget 负责界面渲染,而状态是可以在运行时改变并影响 UI 的数据。当应用规模变大、组件树变深时,跨 Widget 传递和同步状态会变得非常痛苦。状态管理方案的目标就是让状态可预测、可维护、可测试。
Flutter 官方最早推荐的是 setState,但它只适合极小的局部状态。之后 Provider 成为事实标准,再后来 Riverpod 作为其演进版本解决了部分痛点。本文将从实际选型的角度,帮你彻底理清它们的区别与适用场景。
Provider 快速回顾:经典但不够完美
Provider 基于 InheritedWidget 封装,是 Flutter 团队推荐的基础状态管理工具。它和 ChangeNotifier 结合使用,构成了 MVVM 式的架构。
核心概念
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// 注入
ChangeNotifierProvider(create: (_) => Counter())
// 消费
context.watch<Counter>().count
- Provider:提供数据的载体。
- ChangeNotifier:可监听的变化模型,需手动调用
notifyListeners。 - Consumer / context.watch:监听变化并重建 UI。
Provider 的优点
- 官方推荐:文档全面,社区资源丰富。
- 简单易懂:学习曲线平缓,适合 MVVM 初学者。
- 与 Widget 树绑定:自然理解作用域,自动释放资源。
Provider 的典型痛点
- 依赖
BuildContext
在路由外或异步操作中,很难获取到正确的 context,经常出现ProviderNotFoundException。 - 类型不安全的多 Provider 嵌套
多个 Provider 嵌套时,依靠类型区分,容易写错,且不容易静态检查。 ChangeNotifier的局限性
多个状态合并时会导致不必要的重建,需要手动优化(Selector或拆分 Provider)。- 难以做到无上下文访问
不能在纯 Dart 类(如网络层)直接读取状态,必须传递 context。
这些问题在复杂项目中会逐步放大,Riverpod 正是为此而生。
Riverpod 设计哲学:完全去 BuildContext 化
Riverpod 由 Provider 的作者 Remi Rousselet 创建,核心思想是将状态提供者和消费者完全脱离 Widget 树,通过编译时安全的引用(Ref)来读写状态。
核心概念
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter();
});
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
// 消费
final count = ref.watch(counterProvider);
- Provider:全局声明的常量,不会随 Widget 销毁而销毁(除非用
autoDispose)。 - Ref:与 ConsumerWidget 绑定的引用,提供
watch、read、listen等方法。 - 多种变体:
StateProvider、StateNotifierProvider、FutureProvider、StreamProvider等。
Riverpod 的核心优势
- 零
BuildContext依赖
可以在任何地方(比如网络层、数据库层)通过ref.read获取状态,彻底解决上下文地狱。 - 编译安全
所有 Provider 都是常量,通过代码生成保证类型安全,消除了运行时类型错误。 - 细粒度重建
watch只会订阅实际使用的字段,并且 Provider 可以依赖其他 Provider,自动计算并缓存。 - 更好的测试性
可以轻松覆盖 Provider 的行为,无需构建 Widget 树直接进行单元测试。 - 独立的生命周期
通过autoDispose自动回收,或通过keepAlive保持状态,完全可控。
Riverpod 的分包策略
- riverpod:纯 Dart 包,不依赖 Flutter,可同时用于服务器端。
- flutter_riverpod:添加了 Flutter 的
ConsumerWidget等。 - riverpod_generator:代码生成,简化语法并提供更好的静态分析。
详细对比:Provider vs Riverpod
| 维度 | Provider | Riverpod |
|---|---|---|
| 上下文依赖 | 强依赖 BuildContext |
完全无关,使用 Ref |
| 类型安全 | 运行时检查,可能出错 | 编译时检查,常量 Provider |
| 代码位置 | 通常放在 Widget 树中 | 全局声明(final 常量) |
| 生命周期控制 | 随 Provider Widget 的创建销毁 | 支持 autoDispose,可手动控制 |
| 异步处理 | 需结合 FutureBuilder 等 |
内置 FutureProvider,直接消费异步状态 |
| 性能优化 | 需要 Selector 或 context.select |
自动细粒度订阅,依赖图缓存 |
| 测试 | 必须包裹 ProviderScope 和 Widget 树 |
可以单独覆盖 Provider,无需 Widget |
| 学习曲线 | 低,概念少 | 中等,概念多但更系统 |
| 社区与生态 | 成熟,大量教程和插件 | 快速增长,官方推荐的新项目首选 |
何时选 Provider,何时选 Riverpod?
适合选用 Provider 的场景
- 现有项目大量使用 Provider,迁移成本高,团队熟悉其用法。
- 极简单的应用(2~3 个页面,无复杂异步),
setState不够时 Provider 完全胜任。 - 强依赖
InheritedWidget集成的第三方库,尚未提供 Riverpod 适配。 - 对 BuildContext 依赖不感到困扰的快速原型开发。
推荐使用 Riverpod 的明确信号
- 新启动的中大型项目,希望拥有现代化、健壮的状态管理架构。
- 需要跨非 Widget 层(如 Dio 拦截器、数据库层)访问状态。
- 异步状态管理频繁(网络请求、缓存同步),需要方便地合并、处理加载/错误/数据三种状态。
- 对单元测试有较高要求,希望不依赖 UI 框架测试业务逻辑。
- 团队追求编译安全、代码可读性,厌恶运行时异常。
迁移指南:从 Provider 到 Riverpod 的平滑过渡
如果原有项目使用 Provider,不必立即重构。可以渐进引入 Riverpod,两者可以在同一个 ProviderScope 下共存。
第一步:添加依赖
dependencies:
flutter_riverpod: ^2.5.0
# 可选,代码生成
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.4.0
第二步:顶层包裹
将原来的 MultiProvider 替换为 ProviderScope:
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
第三步:改写一个简单 Provider
// 原来的 ChangeNotifierProvider
final counterProvider = ChangeNotifierProvider((_) => Counter());
// 改为 Riverpod 的 StateNotifierProvider
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter();
});
第四步:替换消费 Widget
// 从 StatelessWidget 改为 ConsumerWidget
class CounterDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
可以在同一个页面里同时使用 Provider.of 和 ref.watch,逐步迁移。
常见坑点与最佳实践
Provider 使用建议
- 避免在
build方法中直接read状态,除非是事件处理函数(用context.read替代context.watch)。 - 将逻辑放在
ChangeNotifier里,而非 UI 层。 - 复杂选择用
context.select减少重建。 - 注意
dispose时机,避免内存泄漏。
Riverpod 进阶技巧
- 尽量使用
autoDispose:避免状态常驻内存。 - 使用
family修饰符传递参数:如filteredItemsProvider(arg)。 - 组合 Provider 而非嵌套:
final totalProvider = Provider((ref) => ref.watch(a) + ref.watch(b)); - 利用
ref.listen监听状态变化执行副作用(如导航、弹窗)。 - 为复杂异步逻辑使用
AsyncNotifier(Riverpod 2.0+ 引入),更面向对象。
总结:选型是动态决策
没有银弹,但趋势明确:
- Provider 是基础,学习它有助于理解 Flutter 状态管理的本质。
- Riverpod 是未来,它解决了 Provider 的结构性缺陷,尤其适合中大型生产项目。
如果你是个人开发者开始一个新 App,或者团队决定采用现代化架构,直接选 Riverpod。如果你在维护一个用 Provider 写好的商业应用,不必为了换而换,但新功能模块可以尝试用 Riverpod 编写,平滑演进。
状态管理工具只是工具箱的一部分,关键在于清晰的分层和可测试的设计。选择让你和团队最舒适的方案,然后坚持下去,写干净可维护的代码。