在之前的 《註解模式下的 Riverpod 有什麼特別之處》 我們聊過 Riverpod 2.x 的設計和使用原理,同時當時我們就聊到作者已經在開始探索 3.0 的重構方式,而現在隨著 Riverpod 3.0 的發布,riverpod 帶來了許多細節性的變化。
當然,這也帶來了使用方式上的變動。
廢話不多說,首先 Riverpod 3.0 與 2.0 的對比,新增的功能有:
AutoDisposeNotifier
和 Notifier
等介面,API 變得更加統一和簡潔同時 3.0 也引入了一些破壞性改動:
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
等信息接下來我們詳細講解這些變化。
在 Riverpod 3.0 中,Provider 現在預設會自動重試失敗的計算,這意味著如果一個 Provider 因為網路波動、服務暫時不可用等瞬時錯誤而構建失敗,它不会立即报错,而是會自動嘗試重新計算,直到成功為止。
這個功能是預設開啟的,我相信你第一想法就是我不需要,在某些情況下你可能希望禁用或自定義重試邏輯:
ProviderScope
或 ProviderContainer
的頂層進行全局配置,透過設置 retry
參數,可以精細地控制重試邏輯,例如根據錯誤類型或重試次數來決定是否繼續重試,以及重試的間隔時間:void main() {
runApp(
ProviderScope(
// 全局禁用自動重試
retry: (retryCount, error) => null,
child: MyApp(),
),
);
}
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());
},
),
);
}
}
Riverpod 3.0 引入了兩個實驗性功能:
// 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
}
Riverpod 3.0 對其核心 API 進行了大幅簡化和統一,具體有:
在之前的版本中,有大量帶有 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;
}
類似於 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;
}
}
統一在 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 的行為:
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':
identical
): 由於新舊 User
物件是不同的實例(記憶體地址不同),identical
會返回 false
,UI 會重建。==
): 由於我們重寫了 ==
,只要 name
相同,user1 == user2
就會返回 true
。因此,Riverpod 會認為狀態沒有變化,UI 不會重建,從而避免了不必要的刷新。另外,如果你需要自定義這種行為,可以在你的
Notifier
中重寫updateShouldNotify
方法。
這個改動的核心目的是簡化 API 和提升類型安全,保證 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
類不僅負責創建狀態,還負責處理與該狀態相關的副作用,程式碼的可讀性和維護性更高。
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(ProviderObserverContext context, Object? value) {
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 的重構主要圍繞:
AutoDispose
和 Family
的各種變體,統一 Ref
的類型,透過更少的、功能更強大的構建塊來替代大量專用但零散的 API==
進行更新過濾,確保了無論使用哪種 Provider,對應的重建邏輯都是一致那麼,你喜歡 Riverpod 3.0 嗎?