跳到主要內容

從 `ChangeNotifier` 遷移

在 Riverpod 中,ChangeNotifierProvider 用於提供從 pkg:provider 的平滑過渡。

如果您剛剛開始遷移到 pkg:riverpod,請務必閱讀專用指南 (請參閱快速開始)。 本文適用於已經過渡到 Riverpod,但想要徹底放棄 ChangeNotifier 的人們。

總而言之,從 ChangeNotifier 遷移到 AsyncNotifer 需要正規化轉換, 但它極大地簡化了遷移後的程式碼。 另請參閱Why Immutability

以這個(錯誤的)例子為例:

class MyChangeNotifier extends ChangeNotifier {
MyChangeNotifier() {
_init();
}
List<Todo> todos = [];
bool isLoading = true;
bool hasError = false;

Future<void> _init() async {
try {
final json = await http.get('api/todos');
todos = [...json.map(Todo.fromJson)];
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}

Future<void> addTodo(int id) async {
isLoading = true;
notifyListeners();

try {
final json = await http.post('api/todos');
todos = [...json.map(Todo.fromJson)];
hasError = false;
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}
}

final myChangeProvider = ChangeNotifierProvider<MyChangeNotifier>((ref) {
return MyChangeNotifier();
});

該實現顯示了幾個薄弱的設計選擇,例如:

  • 使用 isLoadinghasError 處理不同的非同步情況
  • 需要仔細處理帶有繁瑣的 try/catch/finally 表示式的請求
  • 需要在正確的時間呼叫 notifyListeners 才能使此實現發揮作用
  • 存在不一致或可能不需要的狀態,例如使用空列表進行初始化

請注意這個示例是如何精心設計的,以向新手開發人員展示 ChangeNotifier 如何導致錯誤的設計方案;此外,另一個要點是可變狀態可能比最初承諾的要困難得多。

Notifier/AsyncNotifer 與不可變狀態相結合, 可以帶來更好的設計方案和更少的錯誤。

讓我們看看如何將上述程式碼片段一步一步遷移到最新的 API。

開始遷移​

首先,我們應該宣告新的提供者程式/通知者程式:這需要一些思維過程,這取決於您獨特的業務邏輯。

我們總結一下上面的要求:

  • 狀態用 List<Todo> 表示,透過網路呼叫獲得,不帶引數
  • 狀態還應該公開有關其 loadingerrordata 狀態的資訊
  • 狀態可以透過一些公開的方法進行改變,因此一個函式是不夠的
提示

上述思考過程歸結為回答以下問題:

  1. 是否需要一些副作用?
    • y:使用 Riverpod 的基於類的 API
    • n:使用 Riverpod 的基於函式的 API
  2. State 需要非同步載入嗎?
    • y:讓 build 返回 Future<T>
    • n:讓 build 簡單地返回 T
  3. 是否需要一些引數?
    • y:讓 build (或你的函式)接受它們
    • n:讓 build (或你的函式)不接受額外的引數
資訊

如果您使用的是 codegen,上述思考過程就足夠了。
無需考慮正確的類名及其特定的 API。
@riverpod 僅要求您編寫一個具有返回型別的類,然後就可以開始了。

從技術上講,這裡最合適的是定義一個 AutoDisposeAsyncNotifier<List<Todo>>, 它滿足上述所有要求。讓我們先寫一些虛擬碼。


class MyNotifier extends _$MyNotifier {

FutureOr<List<Todo>> build() {
// TODO ...
return [];
}

Future<void> addTodo(Todo todo) async {
// TODO
}
}
提示

請記住:在 IDE 中使用程式碼片段可以獲得一些指導,或者只是為了加快程式碼編寫速度。 請參閱入門指南

考慮到 ChangeNotifier 的實現,我們不再需要宣告 todos; 這樣的變數是 state,它是用 build 隱式載入的。

事實上,Riverpod 的通知者程式一次可以暴露一個實體。

提示

Riverpod 的 API 是細粒度的;儘管如此,在遷移時, 您仍然可以定義自定義實體來儲存多個值。首先考慮使用 Dart 3 的記錄 來平滑遷移。

初始化​

初始化通知者程式很簡單:只需在 build 內編寫初始化邏輯即可。
我們現在可以擺脫舊的 _init 函式。


class MyNotifier extends _$MyNotifier {

FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
}

相對於舊的 _init ,新的 build 沒有丟失任何內容: 不需要初始化 isLoadinghasError

Riverpod 將透過公開 AsyncValue<List<Todo>> 自動轉換任何非同步提供者程式, 並比兩個簡單的布林標誌更好地處理非同步狀態的複雜性。

事實上,任何 AsyncNotifier 都會有效地使編寫額外的 try/catch/finally 成為處理非同步狀態的反模式。

突變和副作用​

就像初始化一樣,執行副作用時,無需操作布林標誌, 例如 hasError,或編寫額外的 try/catch/finally

下面,我們刪除了所有樣板檔案併成功完全遷移了上面的示例:


class MyNotifier extends _$MyNotifier {

FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');

return [...json.map(Todo.fromJson)];
}

Future<void> addTodo(Todo todo) async {
// optional: state = const AsyncLoading();
final json = await http.post('api/todos');
final newTodos = [...json.map(Todo.fromJson)];
state = AsyncData(newTodos);
}
}
提示

語法和設計方案可能會有所不同,但最終我們只需要編寫我們的請求並隨後更新狀態。 請參閱執行副作用

遷移過程總結

讓我們從操作的角度回顧一下上面應用的整個遷移過程。

  1. 我們已將初始化從建構函式中呼叫的自定義方法移至 build
  2. 我們刪除了 todosisLoadinghasError 屬性:內部 state 就足夠了
  3. 我們已經刪除了所有 try-catch-finally 塊:返回 Future 就足夠了
  4. 我們對副作用應用了相同的簡化(addTodo
  5. 我們已經透過簡單地重新分配 state 應用了突變