在之前的 《注解模式下的 Riverpod 有什么特别之处》我们聊过 Riverpod 2.x 的设计和使用原理,同时当时我们就聊到作者已经在开始探索 3.0 的重构方式,而现在随着 Riverpod 3.0 的发布,riverpod 带来了许多细节性的变化。
当然,这也带来了需要使用方式上的变动。
废话不多说,首先 Riverpod 3.0 与 2.0 的对比,新增还的功能有:
- 自动重试失败的 Provider: 这是 3.0 的一个核心特性,当一个 Provider 出现计算失败时(如网络错误导致),Riverpod 不会立刻报错,而是自动尝试重新计算,从而让对短暂性错误有更强的恢复能力
- 暂停/恢复支持: 当一个 Widget 不在屏幕上时,与之关联的 Provider 监听器现在会自动暂停
- 离线和变更 (Mutation) 支持 (实验性): Riverpod 3.0 引入了对离线数据缓存和 “mutation” 操作的实验性支持,让处理数据持久化和异步操作(如表单提交)变得更加容易
-
简化的 API: 通过合并
AutoDisposeNotifier和Notifier等接口,API 变得更加统一和简洁
同时 3.0 也引入了一些破坏性改动:
-
传统 Provider 的迁移:
StateProvider,StateNotifierProvider和ChangeNotifierProvider这些在 3.0 属于“传统”API ,它们虽然没有被移除,但都被移至一个新的legacy导入路径下,推荐开发者使用新的NotifierAPI -
统一使用
==进行更新过滤: 在 3.0 版本所有的 Provider 都使用==(相等性) 而非identical来判断状态是否发生变化,从而决定是否需要重建 -
简化的
Ref和移除的子类:Ref不再有泛型参数,并且像ProviderRef.state和Ref.listenSelf这样的属性和方法都被移至Notifier,同时所有Ref的子类(如FutureProviderRef)都已被移除,现在可以直接使用Ref -
移除
AutoDispose接口: 自动释放功能被简化,不再需要独立的AutoDisposeProvider,AutoDisposeNotifier等接口,现在所有 Provider 都可以是auto-dispose -
ProviderObserver接口变更:ProviderObserver的方法签名发生了变化,现在传递的是一个ProviderObserverContext对象,其中包含了ProviderContainer和ProviderBase等信息
下面我们详细讲解这些变化。
自动重试失败的 Provider
在 Riverpod 3.0 中,Provider 现在默认会自动重试失败的计算,这意味着如果一个 Provider 因为网络波动、服务暂时不可用等瞬时错误而构建失败,它不会立即报错,而是会自动尝试重新计算,直到成功为止。
这个功能是默认开启的,我相信你第一想法就是我不需要,在某些情况下你可能希望禁用或自定义重试逻辑:
-
全局禁用/自定义: 你可以在
ProviderScope或ProviderContainer的顶层进行全局配置,通过设置retry参数,可以精细地控制重试逻辑,例如根据错误类型或重试次数来决定是否继续重试,以及重试的间隔时间:void main() { runApp( ProviderScope( // 全局禁用自动重试 retry: (retryCount, error) => null, child: MyApp(), ), ); } -
针对特定 Provider 禁用/自定义: 可以在定义单个 Provider 时,通过其
retry参数进行独立的配置:@Riverpod(retry: retry) class TodoList extends _$TodoList { // 从不重试这个特定的 provider static Duration? retry(int retryCount, Object error) => null; @override List<Todo> build() => []; }
暂停/恢复支持
为了优化资源使用,Riverpod 3.0 引入了暂停/恢复机制。当一个 Widget(及其关联的 Provider 监听器)不在屏幕上时,监听器会自动暂停,这个行为是默认启用的,并且看起来不支持全局关闭,你可以通过 Flutter 的 TickerMode 来手动控制监听器的暂停行为:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TickerMode(
enabled: false, // 这会暂停监听器
child: Consumer(
builder: (context, ref, child) {
// 这个 "watch" 将会暂停
// 直到 TickerMode 设置为 true
final value = ref.watch(myProvider);
return Text(value.toString());
},
),
);
}
}
离线和变更 (Mutation) 支持 (实验性)
Riverpod 3.0 引入了两个实验性功能:
- 离线支持: 允许你轻松地将 Provider 的状态持久化,以便在应用重启或离线时恢复
- 变更 (Mutation) 支持: 提供了一种结构化的方式来处理异步操作,例如用户登录、提交表单或任何会改变应用状态的动作
// A mutation to track the "add todo" operation.
// The generic type is optional and can be specified to enable the UI to interact
// with the result of the mutation.
final addTodo = Mutation<Todo>();
// We listen to the current state of the "addTodo" mutation.
// Listening to this will not perform any side effects by itself.
final addTodoState = ref.watch(addTodo);
switch (addTodoState) {
case MutationIdle():
// Show a button to add a todo
case MutationPending():
// Show a loading indicator
case MutationError():
// Show an error message
case MutationSuccess():
// Show the created todo
}
API 变动
Riverpod 3.0 对其核心 API 进行了大幅简化和统一,具体有:
1、合并 AutoDispose 接口
在之前的版本中,有大量带有 AutoDispose 前缀的接口,如 AutoDisposeProvider、AutoDisposeNotifier ,而在 3.0 中这些接口被统一了,现在,你只需要使用 Provider、Notifier 等核心接口:
//**V2.0:**
// 使用 .autoDispose 修饰符
final myProvider = Provider.autoDispose((ref) {
return MyObject();
});
//**V3.0:**
// 1. 对于手写 Provider
final myProvider = Provider(
(ref) => MyObject(),
isAutoDispose: true, // 使用 isAutoDispose 参数
);
// 2. 对于代码生成的 Provider
@Riverpod(keepAlive: false) // keepAlive: false 是默认行为,等同于 autoDispose
int myProvider(MyProviderRef ref) {
return 0;
}
2、移除 FamilyNotifier 变体
类似于 AutoDispose 的简化,FamilyNotifier、FamilyAsyncNotifier 等家族变体也被移除了,现在你只需要使用 Notifier、AsyncNotifier 等核心 Notifier,并通过构造函数来传递参数
final provider = NotifierProvider.family<CounterNotifier, int, String>(CounterNotifier.new);
-class CounterNotifier extends FamilyNotifier<int, String> {
+class CounterNotifier extends Notifier<int> {
+ CounterNotifier(this.arg);
+ final String arg;
@override
- int build(String arg) {
+ int build() {
// 在这里使用 `arg`
return 0;
}
}
3、 Provider 变动
统一在 Riverpod 3.0 中,StateProvider, StateNotifierProvider, 和 ChangeNotifierProvider 被归类为“传统(legacy)”API,这新的 Notifier API 更加灵活、功能更强大,并且与代码生成(code generation)的结合更紧密,可以显著减少样板代码,现在推荐使用:
-
Notifier: 用于替换StateNotifierProvider,管理同步状态,它是一个可以被监听的类,并且可以定义自己的公共方法来修改状态。 -
AsyncNotifier: 用于替换处理异步操作的StateNotifierProvider或FutureProvider,它专门用于管理异步状态(如从网络获取数据),并内置了对加载、数据和错误状态的处理 -
StreamNotifier: 用于替代StreamProvider
V2.0 :
import 'package:flutter_riverpod/legacy.dart'; // 需要使用 legacy 导入
// Before:
final valueProvider = FutureProvider<int>((ref) async {
ref.listen(anotherProvider, (previous, next) {
ref.state++;
});
ref.listenSelf((previous, next) {
print('Log: $previous -> $next');
});
ref.future.then((value) {
print('Future: $value');
});
return 0;
});
V3.0 (新的 Notifier API):
// After
class Value extends AsyncNotifier<int> {
@override
Future<int> build() async {
ref.listen(anotherProvider, (previous, next) {
ref.state++;
});
listenSelf((previous, next) {
print('Log: $previous -> $next');
});
future.then((value) {
print('Future: $value');
});
return 0;
}
}
final valueProvider = AsyncNotifierProvider<Value, int>(Value.new);
可以看到,如果用的是 2.x 的注解,其实并不需要变动什么。
所以,现在推荐的 API 是 Notifier 和 AsyncNotifier,它们是基于类的 Provider,其原理是将状态的定义 (build 方法) 和 修改状态的方法 封装在同一个类中,目的在于:
- 逻辑内聚: 与特定状态相关的所有代码都在一个地方,易于管理
- 代码更简洁: 结合代码生成,你只需要定义一个类,Provider 会被自动创建
- 类型安全: 你可以定义强类型的公共方法来修改状态,而不是直接暴露状态对象本身
4、统一使用 == 进行更新过滤
这个改动统一了 Provider 的行为:
-
identical: 之前它检查两个引用是否指向同一个内存地址,两个内容完全相同的不同对象,identical会返回false -
==(相等性): 现在检查两个对象是否相等,对于自定义类,你可以重写==操作符来定义相等的标准(例如,如果两个User对象的id相同,则认为它们相等)
具体是,在 V2.0 中某些 Provider(如 Provider)使用 identical 来判断状态是否变化,而另一些则使用 ==,这意味着,即使你提供了一个内容相同但实例不同的新对象,前者也不会通知监听者更新,因为它认为对象“没有变化”。
在 V3.0 中,所有 Provider 都默认使用 == 来比较新旧状态,如果新旧状态通过 == 比较后结果为 true,则不会通知监听者进行重建
举个例子,假设你有一个 User 类,并且你已经重写了 == 操作符:
class User {
final String name;
User(this.name);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is User && runtimeType == other.runtimeType && name == other.name;
@override
int get hashCode => name.hashCode
}
现在,有一个 Provider 返回 User 对象:
final userProvider = Provider((ref) => User('John'));
在某个操作后,你让这个 Provider 返回了一个新的 User 实例,但 name 属性仍然是 'John':
-
V2.0 (使用
identical): 由于新旧User对象是不同的实例(内存地址不同),identical会返回false,UI 会重建。 -
V3.0 (使用
==): 由于我们重写了==,只要name相同,user1 == user2就会返回true。因此,Riverpod 会认为状态没有变化,UI 不会重建,从而避免了不必要的刷新。
另外,如果你需要自定义这种行为,可以在你的
Notifier中重写updateShouldNotify方法。
5、 简化的 Ref 和移除的子类
这个改动的核心目的是简化 AP和提升类型安全,保证API 更统一,因为以前根据 Provider 类型的不同(如 Provider vs FutureProvider),ref 的类型也不同(ProviderRef vs FutureProviderRef),它们各自有不同的属性(例如 FutureProviderRef 有一个 .future 属性),这增加了学习成本,而现在所有 ref 都是同一个 Ref 类型,API 更加一致:
V2.0:
// 使用 .autoDispose 修饰符
final myProvider = Provider.autoDispose((ref) {
return MyObject();
});
V3.0:
// 1. 对于手写 Provider
final myProvider = Provider(
(ref) => MyObject(),
isAutoDispose: true, // 使用 isAutoDispose 参数
);
// 2. 对于代码生成的 Provider
@Riverpod(keepAlive: false) // keepAlive: false 是默认行为,等同于 autoDispose
int myProvider(MyProviderRef ref) {
return 0;
}
类似改动让 API 更加统一,你不需要再记忆两套不同的 Provider 名称,同时职责更清晰,像
ref.state或ref.listenSelf这样的操作,本质上是与状态本身的管理相关的,将这些功能移入Notifier类,让Notifier成为状态和其业务逻辑的唯一管理者,而ref则专注于依赖注入(读取其他 providers)。
例如你需要在一个 Provider 内部监听自身状态的变化来执行某些副作用(比如日志记录):
V2.0:
final myProvider = FutureProvider<int>((ref) {
// 使用 ref.listenSelf 监听自身状态变化
ref.listenSelf((previous, next) {
print('Value changed from $previous to $next');
});
return Future.value(0);
});
V3.0:
@riverpod
class MyNotifier extends _$MyNotifier {
@override
Future<int> build() async {
// listenSelf 现在是 Notifier 的一个方法
listenSelf((previous, next) {
print('Value changed from $previous to $next');
});
return 0;
}
}
可以看到,在 V3.0 中,listenSelf 成为了 MyNotifier 类的一部分,代码的组织结构更加清晰,或者假设你想在一个 Provider 内部,每当其状态更新时,就将新状态持久化到本地存储:
V2.0 (使用 ref.listenSelf):
final counterProvider = FutureProvider<int>((ref) async {
// 在 Provider 内部监听自身
ref.listenSelf((previous, next) {
if (next.hasValue) {
SharedPreferences.getInstance().then((prefs) {
prefs.setInt('counter', next.value!);
});
}
});
// 返回初始值
final prefs = await SharedPreferences.getInstance();
return prefs.getInt('counter') ?? 0;
});
V3.0 (使用 Notifier.listenSelf):
@riverpod
class Counter extends _$Counter {
@override
Future<int> build() async {
// listenSelf 现在是 Notifier 的一个方法
listenSelf((previous, next) {
if (next.hasValue) {
SharedPreferences.getInstance().then((prefs) {
prefs.setInt('counter', next.value!);
});
}
});
final prefs = await SharedPreferences.getInstance();
return prefs.getInt('counter') ?? 0;
}
void increment() async {
state = AsyncData((state.value ?? 0) + 1);
}
}
可以看到,在 V3.0 中逻辑更加内聚,Counter 类不仅负责创建状态,还负责处理与该状态相关的副作用,代码的可读性和维护性更高。
6、 ProviderObserver 接口变更
ProviderObserver 是一个用于监听应用中所有 Provider 变化的强大工具,常用于日志记录或调试,在 V3.0 中它的接口发生了变化:
以前
ProviderObserver的方法会接收provider、value和container等多个独立的参数,现在这些参数被统一封装在一个ProviderObserverContext对象。
V2.0:
class MyObserver extends ProviderObserver {
@override
void didAddProvider(
ProviderBase provider,
Object? value,
ProviderContainer container,
) {
print('Provider ${provider.name ?? provider.runtimeType} was created');
}
}
V3.0:
class MyObserver extends ProviderObserver {
@override
- void didAddProvider(ProviderBase provider, Object? value, ProviderContainer container) {
+ void didAddProvider(ProviderObserverContext context, Object? value) {
- print('Provider ${provider.name ?? provider.runtimeType} was created');
+ print('Provider ${context.provider.name ?? context.provider.runtimeType} was created');
}
}
最后,注解模式并没有被抛弃,而是得到了进一步加强,如果是在 2.x 版本使用了注解模式,那么你的迁移成本会更低,例如:
// Before:
@riverpod
Future<int> value(ValueRef ref) async {
ref.listen(anotherProvider, (previous, next) {
ref.state++;
});
ref.listenSelf((previous, next) {
print('Log: $previous -> $next');
});
ref.future.then((value) {
print('Future: $value');
});
return 0;
}
// After
@riverpod
class Value extends _$Value {
@override
Future<int> build() async {
ref.listen(anotherProvider, (previous, next) {
ref.state++;
});
listenSelf((previous, next) {
print('Log: $previous -> $next');
});
future.then((value) {
print('Future: $value');
});
return 0;
}
}
整体来看, Riverpod 3.0 的重构主要围绕:
-
简化 API ,例如移除
AutoDispose和Family的各种变体,统一Ref的类型,通过更少的、功能更强大的构建块来替代大量专用但零散的 API -
提升一致性 ,通过统一内部行为,让对应 Provider 的表现更加可预测,例如统一使用
==进行更新过滤,确保了无论使用哪种 Provider,对应的重建逻辑都是一致 -
增强功能 ,在不增加复杂度的前提下,引入如自动重试、离线缓存和 Mutation (变更) 支持
那么,你喜欢 Riverpod 3.0 吗?
参考链接
- riverpod.dev/docs/3.0_mi…










