返回列表Flutter Riverpod 深入解析
详解Riverpod Provider类型、ref生命周期、autoDispose/family修饰符、AsyncValue状态处理及测试策略

Flutter Riverpod 深入解析 (Riverpod Advanced)
前言
Riverpod 的名字是 Provider 的字母重新排列(anagram),它由 Provider 的作者 Remi Rousselet 开发,旨在解决 Provider 的根本性问题:
- ProviderNotFoundException:Provider 在正确位置但 context 找不到它
- BuildContext 污染:无法在非 Widget 代码中访问 Provider
- Provider 覆盖困难:无法在同一位置放置两个相同类型的 Provider
- 无法精确控制生命周期:Provider 的生命周期绑定到 Widget 树
Riverpod 通过完全脱离 BuildContext 重新实现了依赖注入,是目前 Flutter 最现代化的状态管理方案。
一、Riverpod 的编译期安全
Provider 的隐患
// ❌ Provider 的常见运行时错误
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 如果 MyModel 的 Provider 没有在 Widget 树的祖先中声明,
// 这里会抛出 ProviderNotFoundException(运行时错误)
final model = context.watch<MyModel>();
return Text(model.value);
}
}
Riverpod 的解决方案
// Riverpod 的 Provider 是全局变量,编译期就能发现引用错误
final myModelProvider = StateNotifierProvider<MyModelNotifier, MyModel>((ref) {
return MyModelNotifier();
});
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ✅ 如果 myModelProvider 不存在,编译期就报错
final model = ref.watch(myModelProvider);
return Text(model.value);
}
}
关键区别:Riverpod 的 Provider 是全局 Dart 变量(通常放在全局或文件顶层),不依赖 Widget 树的位置,只需要 ProviderScope 在根节点存在即可。
二、Provider 的种类详解
2.1 Provider(同步只读值)
Flutter Riverpod深入解析—Provider类型、生命周期与高级模式|新宇宙博客// 提供一个不变的值或派生值
final appVersionProvider = Provider<String>((ref) {
return '1.0.0';
});
// 派生值(依赖其他 Provider)
final fullNameProvider = Provider<String>((ref) {
final user = ref.watch(userProvider);
return '${user.firstName} ${user.lastName}';
});
2.2 StateProvider(简单可变状态)
// 适合简单的基本类型状态(int、bool、String 等)
final counterProvider = StateProvider<int>((ref) => 0);
final isDarkModeProvider = StateProvider<bool>((ref) => false);
final selectedTabProvider = StateProvider<int>((ref) => 0);
// 读取和修改
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('$count'),
ElevatedButton(
// ref.read(provider.notifier).state = 新值
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Text('+1'),
),
],
);
}
}
2.3 StateNotifierProvider(复杂可变状态)
// 购物车状态
class CartNotifier extends StateNotifier<CartState> {
CartNotifier() : super(const CartState());
void addItem(CartItem item) {
state = state.copyWith(
items: [...state.items, item],
);
}
void removeItem(String id) {
state = state.copyWith(
items: state.items.where((i) => i.id != id).toList(),
);
}
}
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
return CartNotifier();
});
// 使用
ref.watch(cartProvider); // 获取 CartState
ref.read(cartProvider.notifier).addItem(item); // 调用方法
2.4 FutureProvider(异步数据)
// 自动处理 loading/data/error 三种状态
final userProvider = FutureProvider.autoDispose<User>((ref) async {
final userId = ref.watch(currentUserIdProvider);
return await UserRepository().fetchUser(userId);
});
// 消费
class UserProfile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (e, stack) => Text('Error: $e'),
);
}
}
// 手动刷新
ref.refresh(userProvider); // 重新触发 Future
// 或:
ref.invalidate(userProvider); // 标记为无效,下次 watch 时重新请求
2.5 StreamProvider(流式数据)
// 适合实时数据(WebSocket、Firestore、传感器等)
final messagesProvider = StreamProvider.autoDispose<List<Message>>((ref) {
return ChatRepository().messageStream();
});
// 与 FutureProvider 一样,用 .when 消费
class ChatPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final messagesAsync = ref.watch(messagesProvider);
return messagesAsync.when(
data: (messages) => MessageList(messages: messages),
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorWidget(e),
);
}
}
2.6 NotifierProvider(Riverpod 2.x 推荐)
Riverpod 2.0 引入了 Notifier 和 AsyncNotifier,是 StateNotifier 的替代品:
// Notifier:同步状态
class CartNotifier extends Notifier<CartState> {
@override
CartState build() {
// build 方法相当于初始值
return const CartState();
}
void addItem(CartItem item) {
state = state.copyWith(items: [...state.items, item]);
}
}
final cartProvider = NotifierProvider<CartNotifier, CartState>(CartNotifier.new);
// AsyncNotifier:异步状态
class ProductsNotifier extends AsyncNotifier<List<Product>> {
@override
Future<List<Product>> build() async {
// 可以在这里 watch 其他 Provider
final category = ref.watch(selectedCategoryProvider);
return await ProductRepository().fetchProducts(category: category);
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => build());
}
}
final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>(
ProductsNotifier.new,
);
三、autoDispose:生命周期管理
autoDispose 修饰符让 Provider 在不再被监听时自动销毁:
// 没有 autoDispose:Provider 一旦创建永久存在
final userProvider = FutureProvider<User>((ref) => fetchUser());
// 有 autoDispose:当所有 Consumer 都从树中移除时,Provider 自动销毁
final userProvider = FutureProvider.autoDispose<User>((ref) => fetchUser());
// 场景:用户进入"商品详情页",请求商品数据
// 当用户返回后,如果没有 autoDispose,商品数据的 Provider 仍然存活
// 下次进入时看到的是旧数据(直到被刷新)
// 使用 autoDispose 后:
// 离开页面 → 所有 Consumer 从树中移除 → Provider 自动销毁
// 再次进入 → Provider 重新创建,重新发起请求 → 始终是最新数据
final productDetailProvider = FutureProvider.autoDispose.family<Product, String>((ref, id) async {
return await ProductRepository().fetchProduct(id);
});
keepAlive:选择性保持存活
final searchHistoryProvider = StateNotifierProvider.autoDispose<SearchHistoryNotifier, List<String>>((ref) {
final notifier = SearchHistoryNotifier();
// 有数据时保持存活,即使没有 Consumer 监听
ref.keepAlive(); // 永久保持(直到手动 invalidate)
// 或者:延迟销毁(防止频繁进出页面时重复请求)
final link = ref.keepAlive();
Timer(const Duration(minutes: 5), link.close); // 5 分钟后销毁
return notifier;
});
四、family:参数化 Provider
family 修饰符允许 Provider 接受参数:
// 根据 userId 获取用户信息
final userByIdProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
return await UserRepository().fetchUser(userId);
});
// 使用
class UserCard extends ConsumerWidget {
final String userId;
const UserCard({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userByIdProvider(userId)); // 传入参数
return userAsync.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);
}
}
// 多参数:用记录(Record)或自定义类
final productFilterProvider = FutureProvider.autoDispose
.family<List<Product>, ({String category, int page})>((ref, params) async {
return await ProductRepository().fetchProducts(
category: params.category,
page: params.page,
);
});
// 使用:
ref.watch(productFilterProvider((category: 'electronics', page: 1)));
五、ref.watch vs ref.read vs ref.listen
ref.watch(订阅 + 重建)
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ✅ 在 build 中使用:订阅 Provider,值变化时 Widget 重建
final count = ref.watch(counterProvider);
return Text('$count');
}
}
// ❌ 错误:不能在回调中使用 ref.watch
ElevatedButton(
onPressed: () {
final count = ref.watch(counterProvider); // 错误!回调中不能 watch
},
)
ref.read(仅读取,不订阅)
// ✅ 在回调/事件处理中使用
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++; // 修改状态
},
)
// ✅ 在 initState 等生命周期中使用(通过 ref.read)
// ❌ 不要在 build 中用 ref.read(无法响应变化)
ref.listen(监听变化,执行副作用)
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 监听状态变化,执行副作用(导航、弹窗、Toast 等)
// 注意:listen 必须在 build 方法的顶层调用
ref.listen<AuthState>(authProvider, (previous, next) {
if (next is AuthStateLoggedOut) {
// 退出登录 → 跳转到登录页
Navigator.of(context).pushReplacementNamed('/login');
}
});
return const SomeWidget();
}
}
| 方法 | 订阅? | 触发重建? | 适用场景 |
|---|
ref.watch | ✅ | ✅ | build 方法中获取响应式数据 |
ref.read | ❌ | ❌ | 回调/事件/initState 中读取或修改状态 |
ref.listen | ✅ | ❌ | 监听变化执行副作用(导航、Toast) |
六、Provider 之间的依赖链
Riverpod 的强大之处在于 Provider 可以相互依赖,形成声明式的数据流图:
// 基础数据
final currentUserIdProvider = StateProvider<String?>((ref) => null);
// 依赖 currentUserIdProvider
final currentUserProvider = FutureProvider.autoDispose<User?>((ref) async {
final userId = ref.watch(currentUserIdProvider); // 监听用户 ID
if (userId == null) return null;
return await UserRepository().fetchUser(userId);
});
// 依赖 currentUserProvider
final userCartProvider = FutureProvider.autoDispose<CartState>((ref) async {
final user = await ref.watch(currentUserProvider.future); // 等待 Future 完成
if (user == null) return const CartState();
return await CartRepository().fetchCart(user.id);
});
// 派生状态:依赖多个 Provider
final checkoutSummaryProvider = Provider.autoDispose<CheckoutSummary>((ref) {
final cart = ref.watch(userCartProvider).valueOrNull ?? const CartState();
final user = ref.watch(currentUserProvider).valueOrNull;
final promoCode = ref.watch(promoCodeProvider);
return CheckoutSummary.calculate(
cart: cart,
user: user,
promoCode: promoCode,
);
});
当 currentUserIdProvider 变化时:
currentUserProvider 检测到依赖变化 → 重新请求用户数据
userCartProvider 检测到 user 变化 → 重新请求购物车
checkoutSummaryProvider 自动重新计算
这就是声明式数据流的优势——你只需要声明数据之间的依赖关系,Riverpod 自动处理更新。
七、实战:带分页、下拉刷新、错误重试的列表页
// 分页参数
@freezed
class ProductsParams with _$ProductsParams {
const factory ProductsParams({
@Default('') String keyword,
@Default(1) int page,
@Default(20) int pageSize,
}) = _ProductsParams;
}
// 分页状态
@freezed
class ProductsState with _$ProductsState {
const factory ProductsState({
@Default([]) List<Product> products,
@Default(false) bool isLoadingMore,
@Default(false) bool hasMore,
@Default(1) int currentPage,
}) = _ProductsState;
}
// Provider
class ProductsNotifier extends AutoDisposeAsyncNotifier<ProductsState> {
@override
Future<ProductsState> build() async {
// 监听搜索关键词
final keyword = ref.watch(searchKeywordProvider);
final products = await ProductRepository().fetchProducts(
keyword: keyword,
page: 1,
);
return ProductsState(
products: products,
hasMore: products.length >= 20,
currentPage: 1,
);
}
// 加载更多
Future<void> loadMore() async {
final currentState = state.valueOrNull;
if (currentState == null || !currentState.hasMore || currentState.isLoadingMore) return;
// 使用 update 避免全局 loading 状态
state = AsyncData(currentState.copyWith(isLoadingMore: true));
try {
final nextPage = currentState.currentPage + 1;
final newProducts = await ProductRepository().fetchProducts(
keyword: ref.read(searchKeywordProvider),
page: nextPage,
);
state = AsyncData(currentState.copyWith(
products: [...currentState.products, ...newProducts],
hasMore: newProducts.length >= 20,
currentPage: nextPage,
isLoadingMore: false,
));
} catch (e) {
state = AsyncData(currentState.copyWith(isLoadingMore: false));
}
}
// 下拉刷新
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => build());
}
}
final productsNotifierProvider =
AsyncNotifierProvider.autoDispose<ProductsNotifier, ProductsState>(
ProductsNotifier.new,
);
// 搜索关键词(与列表解耦)
final searchKeywordProvider = StateProvider.autoDispose<String>((ref) => '');
// UI
class ProductListPage extends ConsumerStatefulWidget {
@override
ConsumerState<ProductListPage> createState() => _ProductListPageState();
}
class _ProductListPageState extends ConsumerState<ProductListPage> {
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
ref.read(productsNotifierProvider.notifier).loadMore();
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final productsAsync = ref.watch(productsNotifierProvider);
return Scaffold(
appBar: AppBar(
title: TextField(
decoration: const InputDecoration(hintText: '搜索商品'),
onChanged: (value) => ref.read(searchKeywordProvider.notifier).state = value,
),
),
body: productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败:$e'),
ElevatedButton(
onPressed: () => ref.read(productsNotifierProvider.notifier).refresh(),
child: const Text('重试'),
),
],
),
),
data: (state) => RefreshIndicator(
onRefresh: () => ref.read(productsNotifierProvider.notifier).refresh(),
child: ListView.builder(
controller: _scrollController,
itemCount: state.products.length + (state.isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.products.length) {
return const Center(child: CircularProgressIndicator());
}
return ProductCard(product: state.products[index]);
},
),
),
),
);
}
}
八、Riverpod 如何解决 ProviderNotFoundException
// Provider(会出现 ProviderNotFoundException)
class PageA extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ❌ 如果没有在合适的祖先节点提供 MyModel,运行时报错
final model = context.watch<MyModel>();
return Text(model.value);
}
}
// Riverpod(不可能出现 ProviderNotFoundException)
final myModelProvider = StateNotifierProvider<MyModelNotifier, MyModel>((ref) {
return MyModelNotifier();
});
class PageA extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ✅ Provider 是全局的,只要 ProviderScope 在根节点存在即可
// 如果 myModelProvider 这个变量不存在,编译期就报错
final model = ref.watch(myModelProvider);
return Text(model.value);
}
}
九、测试:Riverpod 的可测试性
// 测试时使用 ProviderContainer 替代 ProviderScope
test('购物车添加商品', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
// 初始状态
expect(container.read(cartProvider).items, isEmpty);
// 添加商品
container.read(cartProvider.notifier).addItem(
CartItem(product: Product(id: '1', name: '苹果', price: 5.0), quantity: 1),
);
// 验证结果
expect(container.read(cartProvider).items.length, 1);
expect(container.read(cartProvider).total, 5.0);
});
// 覆盖 Provider 进行 Mock
test('用户加载成功', () async {
final container = ProviderContainer(
overrides: [
// 用 Mock 覆盖真实的 Repository
userRepositoryProvider.overrideWithValue(MockUserRepository()),
],
);
final user = await container.read(currentUserProvider.future);
expect(user?.name, 'Mock User');
});
// Widget 测试
testWidgets('计数器点击增加', (tester) async {
await tester.pumpWidget(
ProviderScope(
// 在 Widget 测试中覆盖 Provider 初始值
overrides: [
counterProvider.overrideWith((ref) => 10), // 初始值为 10
],
child: const MaterialApp(home: CounterWidget()),
),
);
expect(find.text('10'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('11'), findsOneWidget);
});
十、autoDispose + keepAlive 缓存策略
// 场景:商品详情页,进入时加载,离开后保持一段时间的缓存
final productDetailProvider = FutureProvider.autoDispose.family<Product, String>((ref, productId) async {
// 设置 5 分钟缓存
final link = ref.keepAlive();
Timer(const Duration(minutes: 5), link.close);
return await ProductRepository().fetchProduct(productId);
});
// 场景:搜索历史,用户使用过就一直保留
final searchHistoryProvider = NotifierProvider.autoDispose<SearchHistoryNotifier, List<String>>(() {
return SearchHistoryNotifier();
});
class SearchHistoryNotifier extends AutoDisposeNotifier<List<String>> {
@override
List<String> build() {
ref.keepAlive(); // 永久保持
return [];
}
void addQuery(String query) {
state = [query, ...state.where((q) => q != query)].take(10).toList();
}
}
过关自检
Q1:Riverpod 如何解决 Provider 的 ProviderNotFoundException 问题?
Provider 依赖 Widget 树中的 BuildContext 来查找 InheritedWidget,如果某个 Widget 的 Provider 没有在祖先节点中声明,运行时就会抛出 ProviderNotFoundException。
Riverpod 的 Provider 是全局 Dart 变量,不依赖 Widget 树位置。ProviderScope 只需在应用根节点放置一次。访问 Provider 时使用 WidgetRef(而非 BuildContext),Riverpod 维护了一个独立的 Provider 容器(ProviderContainer)。如果 Provider 变量不存在,编译期就会报错,彻底消除了运行时的 ProviderNotFoundException。
Q2:如何正确使用 autoDispose + keepAlive 控制缓存?
autoDispose:当所有监听该 Provider 的 Widget 都从树中移除时,自动销毁 Provider 和其持有的数据
ref.keepAlive():返回一个 KeepAliveLink,调用后 Provider 不会因为没有监听者而销毁
link.close():撤销 keepAlive,之后若没有监听者则自动销毁
- 实践:FutureProvider 加
autoDispose 确保每次进入页面都能拿到最新数据;搜索历史等用 keepAlive() 在会话期间永久保留;商品详情等高频访问数据用 Timer + link.close() 实现 TTL 缓存。
小结
| 概念 | 要点 |
|---|
| 编译期安全 | Provider 是全局变量,无 BuildContext 依赖 |
| Provider 类型 | Provider/StateProvider/NotifierProvider/FutureProvider/StreamProvider |
| autoDispose | 无监听时自动销毁,keepAlive 控制存活时间 |
| family | 参数化 Provider,为每组参数创建独立实例 |
| ref.watch | 订阅 + 重建,只在 build 中使用 |
| ref.read | 仅读取,用于回调/事件处理 |
| ref.listen | 监听变化执行副作用(导航、Toast) |
| 依赖链 | Provider 相互依赖,声明式数据流图 |
| 测试 | ProviderContainer + overrides,无 Widget 即可测试 |