第 7.5 章 Riverpod 入门详解(重点)
📎 先跑起来再读原理
第 7.2 节已经给出一个完整的购物车单文件程序(含
ProviderScope、Notifier、派生 Provider)。建议先把它跑起来,再回来读本章的原理与六种 Provider 的区别。Riverpod 是 Flutter 社区目前最主流的状态管理库(pub.dev 点赞数第一梯队),由 Provider 作者 Remi Rousselet 重写。它编译期安全(类型错误直接编译不过)、不依赖 BuildContext、天然支持异步状态与测试。
7.5.1 安装与最小示例
# pubspec.yaml(版本以 pub.dev 上 flutter_riverpod 最新稳定版为准)
dependencies:
flutter_riverpod: ^3.0.0
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ① 定义 Provider:全局常量,类型是 Provider<int>
final counterProvider = Provider<int>((ref) => 42);
// ② 根组件包 ProviderScope(整个应用只能有一个)
void main() => runApp(const ProviderScope(child: MaterialApp(home: Home())));
// ③ 用 ConsumerWidget 替代 StatelessWidget,build 里多出 ref 参数
class Home extends ConsumerWidget {
const Home({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(counterProvider); // 监听:值变则重建
return Scaffold(body: Center(child: Text('值 = $value')));
}
}
7.5.2 六种 Provider,各管一件事
| 类型 | 用途 | 状态可变? | 典型场景 |
|---|---|---|---|
Provider | 只读值 / 依赖注入 | 否 | 配置、Repository、Service |
StateProvider | 简单可变值 | 是(整体替换) | TabIndex、开关、筛选标记 |
NotifierProvider | 复杂可变状态(推荐主力) | 是(方法修改) | 购物车、表单、列表增删改 |
FutureProvider | 一次性异步结果 | 否(可 refresh) | 读一篇文章、请求配置 |
StreamProvider | 持续异步流 | 随流更新 | 倒计时、WebSocket、传感器 |
AsyncNotifierProvider | 异步 + 可变(如分页列表) | 是 | 带刷新/加载更多的列表 |
7.5.3 NotifierProvider:日常开发的主力(重点掌握)
// ① 写一个 Notifier 类:状态 + 修改状态的方法
class CartNotifier extends Notifier<int> {
@override
int build() => 0; // 初始状态
void add() => state++; // state 就是当前状态,直接改
void reset() => state = 0;
}
// ② 挂到 NotifierProvider 上
final cartProvider = NotifierProvider<CartNotifier, int>(CartNotifier.new);
// ③ 界面里 watch + 调用方法
class CartPage extends ConsumerWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(cartProvider);
return Scaffold(
body: Center(child: Text('购物车 $count 件')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(cartProvider.notifier).add(), // read.notifier 拿到对象调方法
child: const Icon(Icons.add),
),
);
}
}
class Todo {
const Todo({required this.title, this.done = false});
final String title;
final bool done;
Todo copyWith({String? title, bool? done}) =>
Todo(title: title ?? this.title, done: done ?? this.done);
}
// 不可变列表状态:每次修改都生成新 List(Riverpod 依赖 == 判断是否刷新)
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 (var i = 0; i < state.length; i++)
i == index ? state[i].copyWith(done: !state[i].done) : state[i],
];
void remove(int index) => state = [...state]..removeAt(index);
}
final todoProvider = NotifierProvider<TodoNotifier, List<Todo>>(TodoNotifier.new);
class TodoPage extends ConsumerWidget {
const TodoPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoProvider);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (_, i) => CheckboxListTile(
value: todos[i].done,
onChanged: (_) => ref.read(todoProvider.notifier).toggle(i),
title: Text(todos[i].title,
style: TextStyle(decoration: todos[i].done ? TextDecoration.lineThrough : null)),
secondary: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => ref.read(todoProvider.notifier).remove(i),
),
),
);
}
}
💡 watch 与 read 的铁律
ref.watch(provider) 写在 build 方法体里——「我要盯着这个状态」;ref.read(provider.notifier).xxx() 写在回调函数体里——「我要执行一个动作」。在 build 里 read、在回调里 watch 都是新手常见反模式。
7.5.4 FutureProvider / StreamProvider:异步状态开箱即用
final userProvider = FutureProvider<Map<String, dynamic>>((ref) async {
final resp = await http.get(Uri.parse('https://api.example.com/user/1'));
return jsonDecode(resp.body);
});
class UserPage extends ConsumerWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when( // AsyncValue 三态自动映射到 UI
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败: $e')),
data: (user) => Text('欢迎,${user['name']}'),
);
}
}
// 手动刷新:ref.invalidate(userProvider); 或 ref.refresh(userProvider);
不需要自己写 loading 布尔标记、try-catch 三件套——AsyncValue.when 把「加载中/失败/成功」三种 UI 一网打尽,这是 Riverpod 最省心的地方。
7.5.5 Provider 组合与传参(family)
// Provider 可以依赖其他 Provider,自动建立依赖关系
final repoProvider = Provider((ref) => UserRepository());
// family:同一个 Provider 按参数缓存多份(如「按 id 请求详情」)
final detailProvider = FutureProvider.family<Detail, int>((ref, id) async {
final repo = ref.watch(repoProvider); // 复用上面的依赖
return repo.fetchDetail(id);
});
// 使用:ref.watch(detailProvider(42));
7.5.6 实用建议与常见坑
- Provider 放全局顶层(文件级 final 常量),不要放在 Widget 类里。
- 状态类保持不可变:修改状态 = 用
copyWith/展开运算符生成新对象,而不是原地改字段。 - 性能优化:
ref.watch(cartProvider.select((c) => c.count))只监听某个字段,字段没变就不重建。 - 页面离开自动清理:给 Provider 加
.autoDispose修饰(如FutureProvider.autoDispose),最后一个监听者离开时自动释放并重置。 - 代码生成(可选进阶):安装
riverpod_annotation + riverpod_generator后用@riverpod注解自动生成样板代码,团队协作时值得引入;初学阶段先手写理解原理。 - 调试神器:DevTools 里有 Riverpod 专用插件页,可实时查看所有 Provider 的状态树。
✅ Riverpod vs Provider vs Bloc vs GetX 怎么选?
新项目个人与中小团队:Riverpod(生态最活跃、官方文档最全);已用 Provider 的老项目继续用没问题;大型企业项目有严格事件驱动规范可考虑 Bloc;GetX 上手最快但「全局函数式」设计争议较大、不利于测试。本手册后续实战采用 Riverpod。