在之前的 《注解模式下的 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
導入路徑下,推薦開發者使用新的Notifier
API - 統一使用
==
進行更新過濾: 在 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
參數進行獨立的配置:(retry: retry) class TodoList extends _$TodoList {// 從不重試這個特定的 providerstatic Duration? retry(int retryCount, Object error) => null; List<Todo> build() => []; }
暫停/恢復支持
為了優化資源使用,Riverpod 3.0 引入了暫停/恢復機制。當一個 Widget(及其關聯的 Provider 監聽器)不在屏幕上時,監聽器會自動暫停,這個行為是默認啟用的,并且看起來不支持全局關閉,你可以通過 Flutter 的 TickerMode
來手動控制監聽器的暫停行為:
class MyWidget extends StatelessWidget { Widget build(BuildContext context) {return TickerMode(enabled: false, // 這會暫停監聽器child: Consumer(builder: (context, ref, child) {// 這個 "watch" 將會暫停// 直到 TickerMode 設置為 truefinal 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 todocase MutationPending():// Show a loading indicatorcase MutationError():// Show an error messagecase 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
(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> { 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); bool operator ==(Object other) =>identical(this, other) ||other is User && runtimeType == other.runtimeType && name == other.name; 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
(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:
class MyNotifier extends _$MyNotifier { 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
):
class Counter extends _$Counter { 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 {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:
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
class Value extends _$Value { 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 嗎?
參考鏈接
- https://riverpod.dev/docs/3.0_migration