從 `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();
});
該實現顯示了幾個薄弱的設計選擇,例如:
- 使用
isLoading
和hasError
處理不同的非同步情況 - 需要仔細處理帶有繁瑣的
try
/catch
/finally
表示式的請求 - 需要在正確的時間呼叫
notifyListeners
才能使此實現發揮作用 - 存在不一致或可能不需要的狀態,例如使用空列表進行初始化
請注意這個示例是如何精心設計的,以向新手開發人員展示 ChangeNotifier
如何導致錯誤的設計方案;此外,另一個要點是可變狀態可能比最初承諾的要困難得多。
Notifier
/AsyncNotifer
與不可變狀態相結合,
可以帶來更好的設計方案和更少的錯誤。
讓我們看看如何將上述程式碼片段一步一步遷移到最新的 API。
開始遷移
首先,我們應該宣告新的提供者程式/通知者程式:這需要一些思維過程,這取決於您獨特的業務邏輯。
我們總結一下上面的要求:
- 狀態用
List<Todo>
表示,透過網路呼叫獲得,不帶引數 - 狀態還應該公開有關其
loading
、error
和data
狀態的資訊 - 狀態可以透過一些公開的方法進行改變,因此一個函式是不夠的
上述思考過程歸結為回答以下問題:
- 是否需要一些副作用?
y
:使用 Riverpod 的基於類的 APIn
:使用 Riverpod 的基於函式的 API
- State 需要非同步載入嗎?
y
:讓build
返回Future<T>
n
:讓build
簡單地返回T
- 是否需要一些引數?
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
沒有丟失任何內容:
不需要初始化 isLoading
或 hasError
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);
}
}
語法和設計方案可能會有所不同,但最終我們只需要編寫我們的請求並隨後更新狀態。 請參閱執行副作用。
遷移過程總結
讓我們從操作的角度回顧一下上面應用的整個遷移過程。
- 我們已將初始化從建構函式中呼叫的自定義方法移至
build
- 我們刪除了
todos
、isLoading
和hasError
屬性:內部state
就足夠了 - 我們已經刪除了所有
try
-catch
-finally
塊:返回 Future 就足夠了 - 我們對副作用應用了相同的簡化(
addTodo
) - 我們已經透過簡單地重新分配
state
應用了突變