跳到主要內容

執行副作用

到目前為止,我們只看到瞭如何獲取資料(也就是執行 GET HTTP 請求)。
但是副作用(例如 POST 請求)呢?

應用程式通常實現 CRUD(建立、讀取、更新、刪除)API。
執行此操作時,更新請求(通常是 POST)通常還應更新本地快取,以使 UI 反映新狀態。

問題是,我們如何從消費者程式內部更新提供者程式的狀態?
理所當然的,提供者程式不會公開修改其狀態的方法。 這是設計使然,以確保僅以受控方式修改狀態並促進關注點分離。
相反,提供者程式必須顯式公開修改其狀態的方法。

為此,我們將使用一個新概念:通知者程式(Notifiers)。
為了展示這個新概念,讓我們使用一個更高階的例子:待辦事項列表。

定義通知者程式

讓我們從此時我們已經知道的內容開始:一個簡單的 GET 請求。 正如之前在開始你的第一次 provider/network 請求中看到的那樣, 我們可以透過編寫以下內容來獲取待辦事項列表:



class Todo with _$Todo {
factory Todo({
required String description,
(false) bool completed,
}) = _Todo;
}


Future<List<Todo>> todoList(Ref ref) async {
// 模擬一個網路請求。這通常來自真實的 API
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}

現在我們已經獲取了待辦事項列表,讓我們看看如何新增新的待辦事項。
為此,我們需要修改我們的提供者程式,以便它們公開一個公共 API 來修改其狀態。 這是透過將我們的提供者程式轉換為我們所說的“通知者程式”來完成的。

通知者程式是提供者程式的“有狀態小部件”。它們需要對定義提供者程式的語法稍作調整。
此新語法如下:

@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  Result build() {
    <你的業務邏輯在這裡>
  }
 
  <你的方法在這裡>
}
註解

所有提供者程式都必須使用 @riverpod@Riverpod() 進行註解。 此註解可以放置在全域性函式或類上。 透過此註解,可以配置提供者程式。

例如,我們可以透過編寫 @Riverpod(keepAlive: true) 來停用“自動處置”(我們將在後面看到)。

通知者程式

@riverpod 註解被放置在一個類上時,該類被稱為“通知者程式”。
類必須擴充套件 _$NotifierName ,其中 NotifierName 是類名。

通知者程式負責公開修改提供者程式狀態的方法。
使用者可以使用 ref.read(yourProvider.notifier).yourMethod() 此類上的公共方法。

備註

除了內建的 state 之外,通知者程式不應具有公共屬性,因為 UI 無法知道狀態已更改。

build 方法

所有通知者程式都必須重寫該 build 方法。
此方法等效於通常將邏輯放在非通知者程式提供者程式中的位置。

不應直接呼叫此方法。

作為參考,您可能需要檢視開始你的第一次 provider/network 請求,將這裡的新語法與之前看到的語法進行比較。

資訊

除了 build 以外,沒有其他方法的通知者程式與使用前面看到的語法相同。

開始你的第一次 provider/network 請求中顯示的語法可以被視為通知者程式的簡寫,無法從 UI 進行修改。

現在我們已經瞭解了語法,讓我們看看如何將之前定義的提供者程式轉換為通知者程式:



class TodoList extends _$TodoList {

Future<List<Todo>> build() async {
// 我們之前在 FutureProvider 中的業務邏輯現在位於 build 方法中。
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}
}

請注意,在小部件中讀取提供者程式的方式保持不變。
您仍然可以像以前的語法一樣使用 ref.watch(todoListProvider)

警告

不要將邏輯放在通知者程式的建構函式中。
通知者程式不應具有建構函式,因為 ref 此時其他屬性尚不可用。 相反,將您的邏輯放在方法中 build

class MyNotifier extends ... {
MyNotifier() {
// ❌ 別這樣做
// 這將會丟擲一個異常
state = AsyncValue.data(42);
}


Result build() {
// ✅ 應該這樣做
state = AsyncValue.data(42);
}
}

公開用於執行 POST 請求的方法

現在我們有了通知者程式,我們可以開始新增方法來執行副作用。 其中一個副作用是讓客戶端 POST 一個新的待辦事項。 我們可以透過在通知者程式上新增一個 addTodo 方法來做到這一點:



class TodoList extends _$TodoList {

Future<List<Todo>> build() async => [/* ... */];

Future<void> addTodo(Todo todo) async {
await http.post(
Uri.https('your_api.com', '/todos'),
// 我們序列化 Todo 物件並將其 POST 到伺服器。
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
}
}

然後,我們可以在 UI 中使用我們在開始你的第一次 provider/network 請求中看到的相同 Consumer/ConsumerWidget 呼叫此方法:

class Example extends ConsumerWidget {
const Example({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// Using "ref.read" combined with "myProvider.notifier", we can
// obtain the class instance of our notifier. This enables us
// to call the "addTodo" method.
ref
.read(todoListProvider.notifier)
.addTodo(Todo(description: 'This is a new todo'));
},
child: const Text('Add todo'),
);
}
}
資訊

請注意我們如何使用 ref.read 而不是呼叫 ref.watch 的方法。 雖然在技術上可以工作,但 ref.watch 建議在事件處理(如“onPressed”)中執行邏輯時使用 ref.read

我們現在有一個按鈕,按下時會發出 POST 請求。
但是,目前,我們的 UI 不會更新以返回新的待辦事項列表。 我們希望本地快取與伺服器的狀態相匹配。

有幾種方法可以做到這一點,下面說說優點和缺點。

更新本地快取以匹配 API 響應

一種常見的後端做法是讓 POST 請求返回資源的新狀態。
特別是,我們的 API 將在新增新的待辦事項後返回新的待辦事項列表。 一種方法是編寫 state = AsyncData(response)

  Future<void> addTodo(Todo todo) async {
// The POST request will return a List<Todo> matching the new application state
final response = await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);

// We decode the API response and convert it to a List<Todo>
List<Todo> newTodos = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>()
.map(Todo.fromJson)
.toList();

// We update the local cache to match the new state.
// This will notify all listeners.
state = AsyncData(newTodos);
}
優點
  • UI 將盡可能具有最新狀態。如果其他使用者添加了待辦事項,我們也會看到它。
  • 伺服器是事實的來源。使用這種方法,客戶端不需要知道需要在列表的哪個位置插入新的待辦事項。
  • 只需要一個網路請求。
缺點
  • 僅當伺服器以特定方式實現時,此方法才有效。如果伺服器不返回新狀態,則此方法將不起作用。
  • 如果關聯的 GET 請求更復雜,例如如果它具有過濾/排序的功能,則可能仍然不可行。

使用 ref.invalidateSelf() 重新整理提供者程式。

一種選擇是讓我們的提供者程式重新執行 GET 請求。
這可以透過在 POST 請求之後呼叫 ref.invalidateSelf() 來完成:

  Future<void> addTodo(Todo todo) async {
// We don't care about the API response
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);

// Once the post request is done, we can mark the local cache as dirty.
// This will cause "build" on our notifier to asynchronously be called again,
// and will notify listeners when doing so.
ref.invalidateSelf();

// (Optional) We can then wait for the new state to be computed.
// This ensures "addTodo" does not complete until the new state is available.
await future;
}
優點
  • UI 將盡可能具有最新狀態。如果其他使用者添加了待辦事項,我們也會看到它。
  • 伺服器是事實的來源。使用這種方法,客戶端不需要知道需要在列表的哪個位置插入新的待辦事項。
  • 無論伺服器實現如何,此方法都應該有效。如果您的 GET 請求更復雜,例如它具有過濾器/排序,則它可能特別有用。
缺點
  • 此方法將執行額外的 GET 請求,這可能效率低下。

手動更新本地快取

另一種選擇是手動更新本地快取。
這將涉及嘗試模仿後端的行為。例如,我們需要知道後端是在開頭還是結尾插入新專案。

  Future<void> addTodo(Todo todo) async {
// We don't care about the API response
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);

// We can then manually update the local cache. For this, we'll need to
// obtain the previous state.
// Caution: The previous state may still be loading or in error state.
// A graceful way of handling this would be to read `this.future` instead
// of `this.state`, which would enable awaiting the loading state, and
// throw an error if the state is in error state.
final previousState = await future;

// We can then update the state, by creating a new state object.
// This will notify all listeners.
state = AsyncData([...previousState, todo]);
}
資訊

此示例使用不可變狀態。這不是必需的,但建議這樣做。 有關更多詳細資訊,請參閱Why Immutability
如果要改用可變狀態,也可以執行以下操作:

    final previousState = await future;
// Mutable the previous list of todos.
previousState.add(todo);
// Manually notify listeners.
ref.notifyListeners();
優點
  • 無論伺服器實現如何,此方法都應該有效。
  • 只需要一個網路請求。
缺點
  • 本地快取可能與伺服器的狀態不匹配。如果其他使用者添加了待辦事項,我們將看不到它。
  • 這種方法的實現和有效地複製後端的邏輯可能更復雜。

更進一步:顯示下拉載入器和錯誤處理

到目前為止我們所看到的一切,我們有一個按鈕,當按下時會發出 POST 請求; 請求完成後,UI 會更新以反映更改。
但目前,沒有跡象表明請求正在執行,如果失敗,也沒有任何資訊。

一種方法是將返回 addTodo 的非同步結果儲存在本地小部件狀態中, 然後監聽該非同步狀態以顯示下拉載入器或錯誤訊息。
這時flutter_hooks派上用場的一種情況。 但是,當然,您也可以使用 StatefulWidget 代替。

以下程式碼片段顯示了當處於載入狀態時,進度指示器和操作處於掛起狀態。 如果失敗,則將按鈕呈現為紅色:

A button which turns red when the operation failed


class Example extends ConsumerStatefulWidget {
const Example({super.key});


ConsumerState<ConsumerStatefulWidget> createState() => _ExampleState();
}

class _ExampleState extends ConsumerState<Example> {
// 待處理的 addTodo 操作。如果沒有待處理的,則為 null。
Future<void>? _pendingAddTodo;


Widget build(BuildContext context) {
return FutureBuilder(
// 我們監聽待處理的操作,以相應地更新 UI。
future: _pendingAddTodo,
builder: (context, snapshot) {
// 計算是否存在錯誤狀態。
// 檢查 connectionState 用於在重試操作時進行處理。
final isErrored = snapshot.hasError &&
snapshot.connectionState != ConnectionState.waiting;

return Row(
children: [
ElevatedButton(
style: ButtonStyle(
// 如果出現錯誤,我們會將該按鈕顯示為紅色
backgroundColor: WidgetStatePropertyAll(
isErrored ? Colors.red : null,
),
),
onPressed: () {
// 我們將 addTodo 返回的 future 儲存在變數中
final future = ref
.read(todoListProvider.notifier)
.addTodo(Todo(description: 'This is a new todo'));

// 我們將這個 future 儲存在本地的狀態中
setState(() {
_pendingAddTodo = future;
});
},
child: const Text('Add todo'),
),
// 操作正在等待,讓我們顯示一個進度指示器
if (snapshot.connectionState == ConnectionState.waiting) ...[
const SizedBox(width: 8),
const CircularProgressIndicator(),
]
],
);
},
);
}
}