從 `StateNotifier` 遷移
與 Riverpod 2.0
一起引入了新類: Notifier
/AsyncNotifer
。
現在不鼓勵使用 StateNotifier
,轉而使用這些新 API。
本頁展示如何從已棄用的 StateNotifier
遷移到新的 API。
AsyncNotifier
帶來的主要好處是更好的 async
支援;事實上,
AsyncNotifier
可以被認為是 FutureProvider
,並且具備從 UI 修改的公開方法。
此外,新的 (Async)Notifier:
- 在其類中公開
Ref
物件 - 在程式碼生成和非程式碼生成方法之間提供類似的語法
- 在同步和非同步版本之間提供類似的語法
- 將邏輯從提供者程式中移開,並將其集中到通知者程式本身中
讓我們看看如何定義 Notifier
、它與 StateNotifier
的比較以及如何遷移新的
AsyncNotifier
以獲得非同步狀態。
新語法比較
在進行比較之前,請務必瞭解如何定義 Notifier
。
請參閱執行副作用。
讓我們使用舊的 StateNotifier
語法編寫一個示例:
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
final counterNotifierProvider =
StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
這是使用新的 Notifier
API 構建的相同示例,大致可翻譯為:
class CounterNotifier extends _$CounterNotifier {
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
比較 Notifier
與 StateNotifier
,可以觀察到以下主要區別:
StateNotifier
的反應式依賴項在其提供者程式中宣告,而Notifier
將此邏輯集中在其build
方法中StateNotifier
的整個初始化過程分為其提供者程式和建構函式, 而Notifier
保留一個位置來放置此類邏輯- 請注意,與
StateNotifier
不同,沒有任何邏輯被寫入Notifier
的建構函式中
使用 AsyncNotifer
、Notifier
的非同步等效項可以得出類似的結論。
遷移非同步 StateNotifier
新 API 語法的主要吸引力在於改進了非同步資料的開發體驗。
舉個例子:
class AsyncTodosNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
AsyncTodosNotifier() : super(const AsyncLoading()) {
_postInit();
}
Future<void> _postInit() async {
state = await AsyncValue.guard(() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
});
}
// ...
}
下面是用新的 AsyncNotifier
API 重寫的上面的示例:
class AsyncTodosNotifier extends _$AsyncTodosNotifier {
FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
// ...
}
AsyncNotifer
與 Notifier
一樣,帶來了更簡單、更統一的 API。
在這裡,很容易將 AsyncNotifer
視為帶有方法的 FutureProvider
。
AsyncNotifer
附帶了一組 StateNotifier
沒有的實用程式和 getter,例如
future
和 update
。
這使我們能夠在處理非同步突變和副作用時編寫更簡單的邏輯。
另請參閱執行副作用。
從 StateNotifier<AsyncValue<T>>
遷移到 AsyncNotifer<T>
歸結為:
- 將初始化邏輯放入
build
- 刪除初始化或副作用方法中的任何
catch
/try
塊 - 從
build
中刪除任何AsyncValue.guard
,因為它將Future
轉換為AsyncValue
優點
在這幾個示例之後,現在讓我們重點介紹 Notifier
和 AsyncNotifer
的主要優點:
- 新語法應該感覺更簡單、更具可讀性,特別是對於非同步狀態
- 一般來說,新 API 的樣板程式碼可能會更少
- 無論您正在編寫哪種型別的提供者程式,語法現在都是統一的,從而支援程式碼生成 (請參閱關於程式碼生成)
讓我們進一步深入並強調更多的差異和相似之處。
顯式 .family
和 .autoDispose
修改
另一個重要的區別是新 API 處理系列和自動處置的方式。
Notifier
,有其自己的 .family
和 .autoDispose
對應項,
例如 FamilyNotifier
和 AutoDisposeNotifier
。
與往常一樣,此類修改可以組合使用(又名 AutoDisposeFamilyNotifier
)。
AsyncNotifer
也有其非同步等效項(例如 AutoDisposeFamilyAsyncNotifier
)。
修改在類中明確說明;所有引數都直接注入 build
方法中,以便初始化邏輯可以使用它們。
這應該會帶來更好的可讀性、更簡潔、總體上更少的錯誤。
以下面的示例為例,其中定義了 StateNotifierProvider.family
。
class BugsEncounteredNotifier extends StateNotifier<AsyncValue<int>> {
BugsEncounteredNotifier({
required this.ref,
required this.featureId,
}) : super(const AsyncData(99));
final String featureId;
final Ref ref;
Future<void> fix(int amount) async {
state = await AsyncValue.guard(() async {
final old = state.requireValue;
final result =
await ref.read(taskTrackerProvider).fix(id: featureId, fixed: amount);
return max(old - result, 0);
});
}
}
final bugsEncounteredNotifierProvider = StateNotifierProvider.family
.autoDispose<BugsEncounteredNotifier, AsyncValue<int>, String>((ref, id) {
return BugsEncounteredNotifier(ref: ref, featureId: id);
});
BugsEncounteredNotifier
感覺...沉重/難以閱讀。
讓我們看一下它的遷移後的 AsyncNotifier
對應部分:
class BugsEncounteredNotifier extends _$BugsEncounteredNotifier {
FutureOr<int> build(String featureId) {
return 99;
}
Future<void> fix(int amount) async {
final old = await future;
final result = await ref.read(taskTrackerProvider).fix(id: this.featureId, fixed: amount);
state = AsyncData(max(old - result, 0));
}
}
其遷移後的版本應該是一本輕鬆的讀物。
(Async)Notifier
的 .family
引數可透過 this.arg
獲取(或使用程式碼生成時的 this.paramName
)
生命週期有不同的行為
Notifier
/AsyncNotifier
和 StateNotifier
之間的生命週期有很大不同。
這個例子再次展示了舊 API 如何具有稀疏邏輯:
class MyNotifier extends StateNotifier<int> {
MyNotifier(this.ref, this.period) : super(0) {
// 1 init logic
_timer = Timer.periodic(period, (t) => update()); // 2 side effect on init
}
final Duration period;
final Ref ref;
late final Timer _timer;
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1); // 3 mutation
if (mounted) state++; // 4 check for mounted props
}
void dispose() {
_timer.cancel(); // 5 custom dispose logic
super.dispose();
}
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
// 6 provider definition
final period = ref.watch(durationProvider); // 7 reactive dependency logic
return MyNotifier(ref, period); // 8 pipe down `ref`
});
在這裡,如果 durationProvider
更新,MyNotifier
會進行處置:
然後重新例項化其例項,然後重新初始化其內部狀態。
此外,與其他提供者程式不同的是,dispose
回撥將在類中單獨定義。
最後,仍然可以在其 provider 中編寫 ref.onDispose
,
再次顯示此 API 的邏輯是多麼稀疏;潛在地,開發人員可能必須研究八 (8!)
個不同的地方才能理解此通知者程式行為!
這些歧義可以透過 Riverpod 2.0
解決。
舊 dispose
與 ref.onDispose
StateNotifier
的 dispose
方法指的是通知者程式本身的 dispose 事件,
也就是在自行處置之前呼叫的回撥。
(Async)Notifier
沒有此屬性,因為它們在重建時不會被處置;只有他們的內部狀態是。
在新的通知者程式中,處置生命週期僅在一個地方處理,透過 ref.onDispose
(和其他),就像任何其他提供者程式一樣。
這簡化了 API,希望也提高了開發體驗,這樣只需檢視一個地方
即可瞭解生命週期的副作用:它的 build
方法。
簡而言之:要註冊在內部狀態重建之前觸發的回撥,
我們可以像其他提供者程式一樣使用 ref.onDispose
。
您可以像這樣遷移上面的程式碼片段:
class MyNotifier extends _$MyNotifier {
int build() {
// Just read/write the code here, in one place
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1);
// `mounted` is no more!
state++; // This might throw.
}
}
在最後一個片段中,肯定有一些簡化,但仍然存在一個未解決的問題:
我們現在無法瞭解我們的通知者程式在執行 update
時是否仍然存在。
這可能會出現不需要的 StateError
。
不再 mounted
發生這種情況是因為 (Async)Notifier
缺少 mounted
屬性,
而該屬性在 StateNotifier
上可用。
考慮到它們生命週期的差異,這是完全有道理的;儘管只是可能,mounted
屬性可能會誤導新通知者程式:mounted
幾乎總是 true
。
雖然可以制定自定義解決方法, 但建議透過取消非同步操作來解決此問題。
可以使用自定義完成器 或任何自定義派生程式來取消操作。
例如,如果您使用 Dio
執行網路請求,請考慮使用取消令牌
(另請參閱清除快取並對狀態處置做出反應)。
因此,上面的示例遷移到以下內容:
class MyNotifier extends _$MyNotifier {
int build() {
// Just read/write the code here, in one place
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
// When `cancelToken.cancel` is invoked, a custom Exception is thrown
state++;
}
}
突變 API 與之前相同
到目前為止,我們已經展示了 StateNotifier
和新 API 之間的差異。
相反, Notifier
、AsyncNotifer
和 StateNotifier
共享的一件事是
如何使用和改變它們的狀態。
消費者程式可以使用相同的語法從這三個提供者程式獲取資料,
這在您從 StateNotifier
遷移時非常有用;這也適用於通知者程式方法。
class SomeConsumer extends ConsumerWidget {
const SomeConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterNotifierProvider);
return Column(
children: [
Text("You've counted up until $counter, good job!"),
TextButton(
onPressed: ref.read(counterNotifierProvider.notifier).increment,
child: const Text('Count even more!'),
)
],
);
}
}
其他遷移
讓我們探討一下 StateNotifier
和 Notifier
(或 AsyncNotifier
)之間影響較小的差異
從 .addListener
和 .stream
遷移
StateNotifier
的 .addListener
和 .stream
可用於監聽狀態更改。
這兩個 API 現在被認為已經過時了。
這是有意為之,因為我們希望與 Notifier
、AsyncNotifier
和其他提供者程式實現完全的 API 統一。
事實上,使用 Notifier
或 AsyncNotifier
應該與任何其他提供者程式沒有任何不同。
因此:
class MyNotifier extends StateNotifier<int> {
MyNotifier() : super(0);
void add() => state++;
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
final notifier = MyNotifier();
final cleanup = notifier.addListener((state) => debugPrint('$state'));
ref.onDispose(cleanup);
// Or, equivalently:
// final listener = notifier.stream.listen((event) => debugPrint('$event'));
// ref.onDispose(listener.cancel);
return notifier;
});
就變成這樣了:
class MyNotifier extends _$MyNotifier {
int build() {
listenSelf((_, next) => debugPrint('$next'));
return 0;
}
void add() => state++;
}
簡而言之:如果你想監聽 Notifier
/AsyncNotifer
,只需使用 ref.listen
。
請參閱組合請求。
從測試中的 .debugState
遷移
StateNotifier
公開 .debugState
:此屬性供 pkg:state_notifier
使用者在開發模式下啟用從類外部進行狀態訪問,以用於測試目的。
如果您在測試中使用 .debugState
訪問狀態,則您很可能需要放棄這種方法。
Notifier
/AsyncNotifer
沒有 .debugState
;相反,它們直接公開 .state
,
即 @visibleForTesting
。
避免!從測試中訪問 .state
;如果必須的話,當且僅當您已經正確例項化了
Notifier
/AsyncNotifer
時才執行此操作;
然後,您可以在測試中自由訪問 .state
。
事實上,Notifier
/AsyncNotifier
不應該手動例項化;相反,
它們應該透過使用其提供者程式進行互動:如果不這樣做將會破壞通知者程式,
因為 ref 和 family 引數沒有被初始化。
沒有 Notifier
例項?
沒問題,您可以使用 ref.read
獲取一個,就像您讀取其暴露狀態一樣:
void main(List<String> args) {
test('my test', () {
final container = ProviderContainer();
addTearDown(container.dispose);
// Obtaining a notifier
final AutoDisposeNotifier<int> notifier = container.read(myNotifierProvider.notifier);
// Obtaining its exposed state
final int state = container.read(myNotifierProvider);
// TODO write your tests
});
}
在其專用指南中瞭解有關測試的更多資訊。請參閱測試你的提供者程式。
從 StateProvider
遷移
StateProvider
自發布以來就被 Riverpod 暴露出來,
它是為了節省一些程式碼行數(LoC)來簡化 StateNotifierProvider
的版本。
由於 StateNotifierProvider
已被棄用,因此 StateProvider
也應避免使用。
此外,到目前為止,新 API 還沒有等效的 StateProvider
。
儘管如此,從 StateProvider
遷移到 Notifier
很簡單。
這樣:
final counterProvider = StateProvider<int>((ref) {
return 0;
});
變成:
class CounterNotifier extends _$CounterNotifier {
int build() => 0;
set state(int newState) => super.state = newState;
int update(int Function(int state) cb) => state = cb(state);
}
儘管它花費了我們更多的程式碼行數(LoC),但從 StateProvider
遷移使我們能夠明確地歸檔 StateNotifier
。