Riverpod
Riverpod 是 Provider 的「升级版」,由 Provider 的同一作者开发。它修复了 Provider 的设计缺陷,提供了更强的类型安全和更灵活的状态管理能力。
Riverpod = River + Pod,是 Provider 的字母重排,意思是「更好的 Provider」。
为什么要学 Riverpod?
| Provider 的问题 | Riverpod 的改进 |
|---|---|
依赖 BuildContext,测试不方便 | 不依赖 BuildContext,随处可用 |
| 运行时才能发现错误 | 编译时就能检查出错误 |
| 多个 Provider 同类型会冲突 | 通过「名称」区分,同类型没问题 |
| 状态不能在 Widget 树外使用 | 状态是全局的,哪里都能用 |
版本说明
本文基于 flutter_riverpod ^3.3.1 编写。Riverpod 经历了多次重大更新:
| 版本 | 说明 |
|---|---|
1.x | 早期版本,StateProvider、StateNotifierProvider 是主力 |
2.x | 引入 NotifierProvider,StateProvider 标记为废弃 |
3.x(当前) | StateProvider 从主库移除,推荐 NotifierProvider / AsyncNotifierProvider |
新旧 API 不要混用
新项目请直接使用新版 API;旧项目如需维护,参考文末 Legacy API 章节。
安装与配置
1. 安装 flutter_riverpod
flutter pub add flutter_riverpod如果你使用 hooks_riverpod(配合 flutter_hooks 使用):
flutter pub add hooks_riverpod本文以
flutter_riverpod为例。
2. 配置 riverpod_lint(推荐)
riverpod_lint 是 Riverpod 官方的 lint 插件,能在编写代码时实时检测常见错误——比如在按钮回调中误用 ref.watch() 而非 ref.read(),编写时直接标红提示。
不了解
analysis_options.yaml?先看 项目结构 → analysis_options.yaml 详解
打开项目根目录的 analysis_options.yaml,加入插件配置:
include: package:flutter_lints/flutter.yaml
plugins:
riverpod_lint: 3.1.3注意
plugins和include是同级的,不要缩进到include里面- 版本号需与
flutter_riverpod匹配,查看 pub.dev 获取最新版本
三个核心概念
在 Riverpod 中,你只需要理解三样东西:
| 概念 | 作用 | 类比 |
|---|---|---|
| Provider | 定义一个状态(数据 + 计算逻辑) | 变量的「定义」 |
| ref | 读取或监听其他 Provider | 变量的「引用」 |
| ProviderScope | 在顶层包裹整个 App | 状态的「容器」 |
快速上手:计数器
第一步:包裹 ProviderScope
在 main() 中用 ProviderScope 包裹你的 App:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// ─── ★ ProviderScope Riverpod 根容器 ──────────────
const ProviderScope(
child: MyApp(),
),
// ─── ☆ ProviderScope Riverpod 根容器 ──────────────
);
}第二步:创建 Provider
// 定义 Notifier 类(Riverpod 3.x 推荐写法)
class CounterNotifier extends Notifier<int> {
@override
int build() => 0; // 初始值
void increment() => state++;
}
// 声明 Provider 变量
final counterProvider = NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);第三步:在 Widget 中使用
用 ConsumerWidget 替代 StatelessWidget,它的 build 方法多一个 ref 参数:
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // 监听变化,值变了自动重建
return Scaffold(
appBar: AppBar(title: const Text('Riverpod 计数器')),
body: Center(
child: Text('计数: $count', style: const TextStyle(fontSize: 48)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(), // 只读一次,不监听
child: const Icon(Icons.add),
),
);
}
}核心规则:watch vs read
这是 Riverpod 最重要的规则,初学者经常搞混:
| 场景 | 用什么 | 原因 |
|---|---|---|
在 build 中显示数据 | ref.watch() | 数据变了需要重建 UI |
| 在按钮点击中修改数据 | ref.read() | 不需要重建按钮本身 |
| 在事件回调中读取一次值 | ref.read() | 只需要当前值 |
// ✅ 正确
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // 显示数据 → watch
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(), // 修改数据 → read
child: Text('$count'),
);
}
// ❌ 错误:在事件回调中用 watch
onPressed: () {
ref.watch(counterProvider); // 不要在 build 之外用 watch!
}一句话记忆
watch 用于「看」,read 用于「做」。配置 riverpod_lint 后,误用会被实时标红提示。
ConsumerWidget vs ConsumerStatefulWidget
| Riverpod Widget | 对应 Flutter Widget | 何时使用 |
|---|---|---|
ConsumerWidget | StatelessWidget | 只需要读取共享状态 |
ConsumerStatefulWidget | StatefulWidget | 既有共享状态,又有自己的内部状态(如输入框文本) |
ConsumerStatefulWidget 的 State 类中可以直接用 ref,也可以用 setState 管理本地状态:
class MyPage extends ConsumerStatefulWidget {
const MyPage({super.key});
@override
ConsumerState<MyPage> createState() => _MyPageState();
}
class _MyPageState extends ConsumerState<MyPage> {
final _controller = TextEditingController(); // 本地状态
@override
Widget build(BuildContext context) {
final cart = ref.watch(cartProvider); // 共享状态
return Text('商品数: ${cart.length}');
}
}Provider 类型速查
| Provider 类型 | 适用场景 | 典型用例 |
|---|---|---|
Provider | 只读值、计算值 | 主题配置、格式化数据 |
NotifierProvider | 同步状态 + 业务逻辑 | 计数器、购物车 |
AsyncNotifierProvider | 异步初始化的状态 + 业务逻辑 | 从本地存储加载配置 |
FutureProvider | 一次性异步数据 | 从 API 获取数据 |
StreamProvider | 持续产生的数据流 | WebSocket、计时器 |
下面逐一讲解。
Provider — 只读值与计算值
最简单的 Provider,用于提供不会自己变化的值,或从其他 Provider 派生计算结果:
// 提供一个固定配置
final themeColorProvider = Provider<Color>((ref) => Colors.blue);
// 从其他 Provider 派生
final uncompletedCountProvider = Provider<int>((ref) {
final todos = ref.watch(todoProvider);
return todos.where((t) => !t.done).length;
});Provider 没有修改状态的方法,只负责「给值」。需要修改状态请用 NotifierProvider。
NotifierProvider — 同步状态管理
当状态有业务逻辑(如增删改),使用 NotifierProvider——Riverpod 3.x 的核心推荐写法。
创建与使用
// 商品模型
class Product {
final String name;
final int price;
const Product({required this.name, required this.price});
}
// 购物车 Notifier
class CartNotifier extends Notifier<List<Product>> {
@override
List<Product> build() => []; // 初始状态:空购物车
void add(Product product) => state = [...state, product];
void remove(int index) => state = [...state]..removeAt(index);
void clear() => state = [];
}
// 创建 Provider
final cartProvider = NotifierProvider<CartNotifier, List<Product>>(
CartNotifier.new,
);// 在 Widget 中使用
class CartPage extends ConsumerWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
final totalPrice = cart.fold<int>(0, (sum, item) => sum + item.price);
return Scaffold(
appBar: AppBar(title: Text('购物车 (${cart.length})')),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.length,
itemBuilder: (context, index) {
final item = cart[index];
return ListTile(
title: Text(item.name),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('¥${item.price}'),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => ref.read(cartProvider.notifier).remove(index),
),
],
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text('总计: ¥$totalPrice',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(cartProvider.notifier).add(const Product(name: '苹果', price: 5)),
child: const Icon(Icons.add),
),
);
}
}不可变更新
在 Notifier 中更新状态时,必须创建新对象(state = [...state, item]),而不是直接修改(state.add(item) ❌)。直接修改不会触发 UI 刷新。
AsyncNotifierProvider — 异步初始化状态
状态的初始值需要异步获取(如从本地存储读取配置),使用 AsyncNotifierProvider。
| 对比 | NotifierProvider | AsyncNotifierProvider |
|---|---|---|
| 初始化 | build() 返回同步值 | build() 返回 Future |
| 读取状态 | 直接拿到值 | 拿到 AsyncValue,需用 .when() 处理 |
class SettingsNotifier extends AsyncNotifier<ThemeMode> {
@override
Future<ThemeMode> build() async {
await Future.delayed(const Duration(seconds: 1)); // 模拟异步读取
return ThemeMode.system;
}
Future<void> setTheme(ThemeMode mode) async {
await Future.delayed(const Duration(milliseconds: 200)); // 模拟异步保存
state = AsyncData(mode);
}
}
final settingsProvider = AsyncNotifierProvider<SettingsNotifier, ThemeMode>(
SettingsNotifier.new,
);// 在 Widget 中使用 AsyncValue.when() 处理三种状态
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settingsState = ref.watch(settingsProvider);
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: settingsState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Text('加载失败: $err'),
data: (mode) => SwitchListTile(
title: const Text('深色模式'),
value: mode == ThemeMode.dark,
onChanged: (isDark) {
ref.read(settingsProvider.notifier).setTheme(
isDark ? ThemeMode.dark : ThemeMode.light,
);
},
),
),
);
}
}FutureProvider — 一次性异步数据
适用于一次请求、加载完成后不再变化的场景,如从 API 获取数据。
Future<String> fetchUserName() async {
await Future.delayed(const Duration(seconds: 2));
return '张三';
}
final userNameProvider = FutureProvider<String>((ref) async {
return fetchUserName();
});Widget 中同样通过 AsyncValue.when() 处理加载/错误/数据三种状态:
class UserPage extends ConsumerWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userName = ref.watch(userNameProvider);
return userName.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('出错了: $err')),
data: (name) => Center(child: Text('欢迎, $name', style: const TextStyle(fontSize: 32))),
);
}
}刷新数据用 ref.invalidate():
TextButton(
onPressed: () => ref.invalidate(userNameProvider),
child: const Text('刷新'),
);StreamProvider — 持续数据流
适用于数据会持续产生的场景,如 WebSocket、计时器、传感器数据。
final tickingSecondsProvider = StreamProvider.autoDispose<int>((ref) async* {
var value = 0;
while (true) {
await Future.delayed(const Duration(seconds: 1));
value++;
yield value;
}
});final secondsState = ref.watch(tickingSecondsProvider);
secondsState.when(
loading: () => const Text('秒表启动中...'),
error: (error, _) => Text('错误:$error'),
data: (value) => Text('已经过 $value 秒'),
);修饰符:autoDispose 与 family
autoDispose — 自动释放
当没有 Widget 监听时,自动销毁 Provider 并释放资源。就像教室的灯:有人时亮着,所有人走了自动关灯省电。
final searchProvider = FutureProvider.autoDispose<List<String>>((ref) async {
final query = ref.watch(queryProvider);
return apiSearch(query); // 页面离开后自动取消
});推荐:网络请求、Stream 等消耗资源的 Provider 都应加
.autoDispose。
family — 接收参数
让 Provider 根据外部参数生成不同的实例:
final articleDetailProvider = FutureProvider.autoDispose.family<String, String>(
(ref, articleId) async {
await Future.delayed(const Duration(seconds: 1));
return '这是 $articleId 的详情内容';
},
);
// 使用时传入参数
final detail = ref.watch(articleDetailProvider('article_001'));组合使用
顺序固定:先 autoDispose,再 family。
// ✅ 正确
FutureProvider.autoDispose.family<String, String>(...)
// ❌ 错误
FutureProvider.family.autoDispose<String, String>(...)Provider 之间互相依赖
Provider 可以依赖其他 Provider,实现自动派生计算:
final productsProvider = Provider<List<Product>>((ref) => [
const Product(name: '苹果', price: 5),
const Product(name: '香蕉', price: 3),
const Product(name: '橘子', price: 4),
]);
final cartProvider = NotifierProvider<CartNotifier, List<Product>>(CartNotifier.new);
// 总价 Provider —— 依赖购物车,自动计算
final totalPriceProvider = Provider<int>((ref) {
final cart = ref.watch(cartProvider);
return cart.fold<int>(0, (sum, item) => sum + item.price);
});购物车内容变化时,totalPriceProvider 自动重新计算:
Text('总计: ¥${ref.watch(totalPriceProvider)}')代码生成方式(进阶)
前面介绍的都是手动写法——手写 Notifier 类 + 手写 Provider 变量。Riverpod 还支持代码生成写法,用注解代替手写模板代码,由工具自动生成 Provider 定义。
初学者建议先掌握手动写法,理解核心概念后再尝试代码生成。正式项目推荐代码生成,这也是 Riverpod 官方推荐的最佳实践。
手动 vs 代码生成对比
// ── 手动写法 ──
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
// ── 代码生成写法 ──
part 'counter.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
// counterProvider 自动生成在 counter.g.dart 中| 手动写法 | 代码生成写法 |
|---|---|
| 手动声明 Provider 变量 | 自动生成 |
| 泛型容易写错 | 自动推断 |
容易忘记加 autoDispose | 默认 autoDispose |
安装
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner生成代码
# 一次性生成
dart run build_runner build
# 监听文件变化,自动重新生成(开发时推荐)
dart run build_runner watch理解 part 指令
代码生成写法中,你一定会看到这行:
part 'counter.g.dart';part 是 Dart 语言的关键字,意思是「这个文件和另一个文件属于同一个库,共享私有成员」。
打个比方:
- 你写了一个章节的正文(
counter.dart) - 有些模板内容让助手帮你写(
counter.g.dart) part告诉 Dart:「我们俩合起来才是完整的一章」
.g.dart 文件由 build_runner 自动生成,里面包含了 Provider 变量声明、基类定义等模板代码。
关键规则
part 'xxx.g.dart';中的文件名必须与当前文件对应:counter.dart→counter.g.dart- 用了
@riverpod注解,就必须加上对应的part声明 - 永远不要手动编辑
.g.dart文件——每次运行build_runner都会重新覆盖 - 继承的基类是
_$Xxx(带下划线前缀),不是Notifier——这个基类也是自动生成的
理解 @riverpod 注解
代码生成提供两种形式:
函数形式——只提供值,不需要修改状态:
@riverpod
int count(Ref ref) => 0;
// 自动生成 countProvider类形式——需要修改状态:
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
// 自动生成 counterProvider| 对比 | 函数形式 | 类形式 |
|---|---|---|
| 能修改状态 | ❌ | ✅ |
| 适用 | 路由、配置、计算值 | 计数器、购物车 |
| 生成 Provider 名 | 函数名 + Provider | 类名(首字母小写)+ Provider |
@riverpod vs @Riverpod(keepAlive: true)
| 写法 | 生命周期 |
|---|---|
@riverpod | 没人监听时自动销毁(autoDispose) |
@Riverpod(keepAlive: true) | 始终存活,直到应用结束 |
什么时候需要 keepAlive: true?
| 场景 | 需要 keepAlive? | 原因 |
|---|---|---|
路由配置 (GoRouter) | ✅ | 应用级基础设施,不能被销毁 |
| 用户登录信息 | ✅ | 全局状态,不能丢 |
| 主题设置 | ✅ | 全局配置,始终需要 |
| 页面搜索结果 | ❌ | 离开页面后不需保留 |
| 商品详情 | ❌ | 看完即走 |
@Riverpod(keepAlive: true)
GoRouter appRouter(Ref ref) {
return GoRouter(
initialLocation: AppRoutes.news,
routes: [/* ... */],
);
}WARNING
拿不准就用默认的 @riverpod(自动销毁,更省内存)。滥用 keepAlive 会导致内存泄漏。
常用注解速查
| 注解 | 作用 | 示例 |
|---|---|---|
@riverpod | 默认 autoDispose 的 Provider | @riverpod class Counter extends _$Counter { ... } |
@Riverpod(keepAlive: true) | 不会被自动释放的 Provider | 全局配置、路由 |
@riverpod(函数) | 只读/计算型 Provider | @riverpod int count(Ref ref) => 0; |
完整示例:待办事项 App
综合前面所学,做一个待办事项应用:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(title: '待办事项', home: const TodoPage());
}
}
// ============ 数据模型 ============
class Todo {
final String title;
final bool done;
const Todo({required this.title, this.done = false});
Todo copyWith({String? title, bool? done}) =>
Todo(title: title ?? this.title, done: done ?? this.done);
}
// ============ 状态管理 ============
class TodoNotifier extends Notifier<List<Todo>> {
@override
List<Todo> build() => [];
void add(String title) => state = [...state, Todo(title: title)];
void toggle(int index) => state = [
for (int i = 0; i < state.length; i++)
if (i == index) state[i].copyWith(done: !state[i].done)
else state[i],
];
void remove(int index) => state = [...state]..removeAt(index);
}
final todoProvider = NotifierProvider<TodoNotifier, List<Todo>>(TodoNotifier.new);
// 派生状态:未完成数量
final uncompletedCountProvider = Provider<int>((ref) {
final todos = ref.watch(todoProvider);
return todos.where((t) => !t.done).length;
});
// ============ UI ============
class TodoPage extends ConsumerStatefulWidget {
const TodoPage({super.key});
@override
ConsumerState<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends ConsumerState<TodoPage> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _addTodo() {
final text = _controller.text.trim();
if (text.isEmpty) return;
ref.read(todoProvider.notifier).add(text);
_controller.clear();
}
@override
Widget build(BuildContext context) {
final todos = ref.watch(todoProvider);
final remaining = ref.watch(uncompletedCountProvider);
return Scaffold(
appBar: AppBar(title: Text('待办事项 ($remaining 未完成)')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: '输入待办事项...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
ElevatedButton(onPressed: _addTodo, child: const Text('添加')),
],
),
),
Expanded(
child: todos.isEmpty
? const Center(child: Text('暂无待办事项'))
: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: Checkbox(
value: todo.done,
onChanged: (_) =>
ref.read(todoProvider.notifier).toggle(index),
),
title: Text(todo.title,
style: TextStyle(
decoration: todo.done
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () =>
ref.read(todoProvider.notifier).remove(index),
),
);
},
),
),
],
),
);
}
}Riverpod vs Provider 快速对比
| 对比项 | Provider | Riverpod |
|---|---|---|
| 定义状态 | 继承 ChangeNotifier | Provider / NotifierProvider |
| 注入方式 | Widget 树中用 Provider 包裹 | 顶层 ProviderScope,Provider 定义在外部 |
| 读取方式 | context.watch() / context.read() | ref.watch() / ref.read() |
| Widget 基类 | StatelessWidget / StatefulWidget | ConsumerWidget / ConsumerStatefulWidget |
| 依赖 BuildContext | 是 | 否 |
| 编译时安全 | 否 | 是 |
| 同类型多个 Provider | 冲突 | 通过名称区分 |
Legacy API — 旧版写法
WARNING
以下 API 在 flutter_riverpod 3.x 中已废弃,从主库移除。如需使用须额外引入 legacy.dart。新项目请直接使用新版 API。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart'; // 旧版 APIStateProvider → 迁移到 NotifierProvider
// ❌ 旧版
final counterProvider = StateProvider<int>((ref) => 0);
// 修改:ref.read(counterProvider.notifier).state++;
// ✅ 新版
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);StateNotifierProvider → 迁移到 NotifierProvider
// ❌ 旧版(需额外引入 state_notifier 包)
class CartNotifier extends StateNotifier<List<Product>> {
CartNotifier() : super([]);
void add(Product product) => state = [...state, product];
}
final cartProvider = StateNotifierProvider<CartNotifier, List<Product>>(
(ref) => CartNotifier(),
);
// ✅ 新版(内置支持,无需额外包)
class CartNotifier extends Notifier<List<Product>> {
@override
List<Product> build() => [];
void add(Product product) => state = [...state, product];
}
final cartProvider = NotifierProvider<CartNotifier, List<Product>>(CartNotifier.new);新旧 API 对照
| 旧版 | 新版 | 说明 |
|---|---|---|
StateProvider<T> | NotifierProvider<XxxNotifier, T> | 简单值 |
StateNotifierProvider<XxxNotifier, T> | NotifierProvider<XxxNotifier, T> | 复杂逻辑 |
| 无 | AsyncNotifierProvider<XxxNotifier, T> | 异步初始化 |
.notifier.state = value | .notifier.method() | 新版通过方法修改,更安全 |
常见问题
1. Provider 没找到?
确保 ProviderScope 包裹了整个 App,且 Provider 定义在 Widget 之外(通常是全局变量)。
2. 状态更新了但 UI 没刷新?
检查是否用了 ref.read() 而不是 ref.watch() 来显示数据。显示数据必须用 watch。
3. Notifier 中修改状态没生效?
状态更新必须是不可变的,要创建新对象:
// ❌ 直接修改
state.add(product); // UI 不会更新!
// ✅ 创建新列表
state = [...state, product]; // UI 会更新4. StateProvider 报错 "The function 'StateProvider' isn't defined"?
StateProvider 在 flutter_riverpod 3.x 中已移除,需额外引入:
import 'package:flutter_riverpod/legacy.dart';或迁移到 NotifierProvider,详见 Legacy API。
5. NotifierProvider 和 AsyncNotifierProvider 怎么选?
- 初始值可以直接确定 →
NotifierProvider - 初始值需要异步获取 →
AsyncNotifierProvider
