跳到主要內容

從 `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--;
}

比較 NotifierStateNotifier,可以觀察到以下主要區別:

  • StateNotifier 的反應式依賴項在其提供者程式中宣告,而 Notifier 將此邏輯集中在其 build 方法中
  • StateNotifier 的整個初始化過程分為其提供者程式和建構函式, 而 Notifier 保留一個位置來放置此類邏輯
  • 請注意,與 StateNotifier 不同,沒有任何邏輯被寫入 Notifier 的建構函式中

使用 AsyncNotiferNotifier 的非同步等效項可以得出類似的結論。

遷移非同步 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)];
}

// ...
}

AsyncNotiferNotifier 一樣,帶來了更簡單、更統一的 API。 在這裡,很容易將 AsyncNotifer 視為帶有方法的 FutureProvider

AsyncNotifer 附帶了一組 StateNotifier 沒有的實用程式和 getter,例如 futureupdate。 這使我們能夠在處理非同步突變和副作用時編寫更簡單的邏輯。 另請參閱執行副作用

提示

StateNotifier<AsyncValue<T>> 遷移到 AsyncNotifer<T> 歸結為:

  • 將初始化邏輯放入 build
  • 刪除初始化或副作用方法中的任何 catch/try
  • build 中刪除任何 AsyncValue.guard ,因為它將 Future 轉換為 AsyncValue

優點​

在這幾個示例之後,現在讓我們重點介紹 NotifierAsyncNotifer 的主要優點:

  • 新語法應該感覺更簡單、更具可讀性,特別是對於非同步狀態
  • 一般來說,新 API 的樣板程式碼可能會更少
  • 無論您正在編寫哪種型別的提供者程式,語法現在都是統一的,從而支援程式碼生成 (請參閱關於程式碼生成

讓我們進一步深入並強調更多的差異和相似之處。

顯式 .family.autoDispose 修改​

另一個重要的區別是新 API 處理系列和自動處置的方式。

Notifier,有其自己的 .family.autoDispose 對應項, 例如 FamilyNotifierAutoDisposeNotifier
與往常一樣,此類修改可以組合使用(又名 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/AsyncNotifierStateNotifier 之間的生命週期有很大不同。

這個例子再次展示了舊 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 解決。

disposeref.onDispose

StateNotifierdispose 方法指的是通知者程式本身的 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 之間的差異。
相反, NotifierAsyncNotiferStateNotifier 共享的一件事是 如何使用和改變它們的狀態。

消費者程式可以使用相同的語法從這三個提供者程式獲取資料, 這在您從 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!'),
)
],
);
}
}

其他遷移​

讓我們探討一下 StateNotifierNotifier(或 AsyncNotifier)之間影響較小的差異

.addListener.stream 遷移​

StateNotifier.addListener.stream 可用於監聽狀態更改。 這兩個 API 現在被認為已經過時了。

這是有意為之,因為我們希望與 NotifierAsyncNotifier 和其他提供者程式實現完全的 API 統一。
事實上,使用 NotifierAsyncNotifier 應該與任何其他提供者程式沒有任何不同。

因此:

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