開始你的第一次 provider/network 請求
網路請求是任何應用程式的核心。但是,在發出網路請求時,需要考慮很多事項:
- UI 應在發出請求時呈現載入狀態
- 應妥善處理錯誤
- 如果可能,應快取請求
在本節中,我們將看到 Riverpod 如何幫助我們自然地處理所有這些問題。
配置 ProviderScope
在開始發出網路請求之前,請確保將其 ProviderScope
新增到應用程式的根目錄。
void main() {
runApp(
// To install Riverpod, we need to add this widget above everything else.
// This should not be inside "MyApp" but as direct parameter to "runApp".
// 為了安裝 Riverpod,我們需要將這個小元件新增到所有的小元件之上。
// 它不應該在 “MyApp” 內部,而是作為 “runApp” 的直接引數。
ProviderScope(
child: MyApp(),
),
);
}
這樣就可以為整個應用程式啟用 Riverpod。
有關完整的安裝步驟(例如安裝 riverpod_lint 和執行程式碼生成器),請檢視入門指南。
在 “provider” 中執行網路請求
執行網路請求通常就是我們所說的“業務邏輯”。在 Riverpod 中,業務邏輯位於“providers”中。
provider 是一種具有超能力的函式。它們的行為與正常函式類似,並具有以下額外好處:
- 保持快取
- 提供預設錯誤/載入處理
- 可以被監聽
- 當某些資料發生變化時自動重新執行
這使得 provider 非常適合 GET 網路請求(與 POST/etc 請求一樣,請參閱執行副作用)。
舉個例子,讓我們做一個簡單的應用程式,建議在無聊時做一個隨機的活動。
為此,我們將使用 Bored API。具體而言,
我們將在 /api/activity
端點上執行 GET 請求。端點返回一個 JSON 物件,我們將把它解析為 Dart 類例項。
然後,下一步是在 UI 中顯示此活動。我們還將確保在發出請求時呈現載入狀態,並優雅地處理錯誤。
聽起來不錯?讓我們開始吧!
定義資料模型
在開始之前,我們需要定義從 API 接收的資料模型。 該模型還需要一種方法將 JSON 物件解析為 Dart 類例項。
通常,建議使用 Freezed 或 json_serializable 等程式碼生成器來處理 JSON 解碼。 雖然但是,也可以手動完成。
無論如何,這是我們的模型:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'activity.freezed.dart';
part 'activity.g.dart';
/// `GET /api/activity` 請求的響應。
///
/// 這個定義使用了 `freezed` 和 `json_serializable`。
sealed class Activity with _$Activity {
factory Activity({
required String key,
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;
/// 將 JSON 物件轉換為 [Activity] 例項。
/// 這可以實現 API 響應的型別安全讀取。
factory Activity.fromJson(Map<String, dynamic> json) =>
_$ActivityFromJson(json);
}
建立 provider
現在我們有了模型,可以開始建立查詢 API。
為此,我們需要建立我們的第一個 provider。
定義 provider 的語法如下:
@riverpod Result myFunction(Ref ref) { <你的邏輯寫這裡> }
註解 | 所有提供者程式都必須使用 例如,我們可以透過編寫 |
帶註解的函式 | 帶批註的函式的名稱決定了如何與提供者程式進行互動。 帶註釋的函式必須指定“ref”作為第一個引數。 首次讀取提供者程式時將呼叫此函式。 |
Ref | 用於與其他提供者程式互動的物件。 |
在我們的例子中,我們希望從 API 中 GET 一個活動。
由於 GET 是非同步操作,這意味著我們需要建立一個 Future<Activity>
。
因此,使用前面定義的語法,我們可以按如下方式定義提供者程式:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'activity.dart';
// 程式碼生成正常工作的必要條件
part 'provider.g.dart';
/// 這將建立一個名為 `activityProvider` 的提供者程式
/// 它可以快取函式執行的結果
Future<Activity> activity(Ref ref) async {
// 使用 package:http, 我們可以從 Bored API 獲取一個隨機的活動。
final response = await http.get(Uri.https('boredapi.com', '/api/activity'));
// 使用 dart:convert, 然後我們將 JSON 有效負載解碼為 Map 資料結構。
final json = jsonDecode(response.body) as Map<String, dynamic>;
// 最後,我們將 Map 轉換為 Activity 例項。
return Activity.fromJson(json);
}
在此程式碼片段中,我們定義了一個名為 activityProvider
的提供者程式,
我們的 UI 將能夠使用該提供者程式來獲取隨機活動。值得一提的是:
- 在 UI 讀取提供者程式至少一次之前,不會執行網路請求。
- 後續讀取不會重新執行網路請求,而是返回之前提取的活動。
- 如果 UI 停止使用此提供者程式,則快取將被處置。 然後,如果 UI 再次使用提供者程式,則會發出新的網路請求。
- 我們沒有捕獲錯誤。這是自動的,因為提供者程式本身會處理錯誤。
如果網路請求或 JSON 解析丟擲錯誤,則 Riverpod 將捕獲該錯誤。 然後,UI 將自動包含呈現錯誤頁面所需的資訊。
提供者程式是“懶惰的”。定義提供者程式不會執行網路請求。 相反,網路請求將在首次讀取提供者程式時執行。
在 UI 中呈現網路請求的響應
現在我們已經定義了一個提供者程式,我們可以開始在 UI 中使用它來顯示活動。
為了與提供者程式互動,我們需要一個名為“ref”的物件。
您之前可能在提供者程式定義中看到過它,因為提供者程式自然可以訪問“ref”物件。
但在我們的例子中,我們不是提供者程式,而是小部件。那麼我們如何獲得“ref”呢?
解決方案是使用名為 Consumer
的自定義小部件。
Consumer
是一個類似於 Builder
的小部件,但還有一個額外的好處,那就是為我們提供了一個“ref”。
這使我們的 UI 能夠讀取提供者程式。以下示例展示瞭如何使用 Consumer
:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'activity.dart';
import 'provider.dart';
/// 我們應用程式主頁
class Home extends StatelessWidget {
const Home({super.key});
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
// 讀取 activityProvider。如果沒有準備開始,這將會開始一個網路請求。
// 透過使用 ref.watch,小元件將會在 activityProvider 更新時重建。
// 當下面的事情發生時,會更新小元件:
// - 響應從“正在載入”變為“資料/錯誤”
// - 請求重重新整理
// - 結果被本地修改(例如執行副作用時)
// ...
final AsyncValue<Activity> activity = ref.watch(activityProvider);
return Center(
/// 由於網路請求是非同步的並且可能會失敗,我們需要處理錯誤和載入的狀態。
/// 我們可以為此使用模式匹配。
/// 我們也可以使用 `if (activity.isLoading) { ... } else if (...)`
child: switch (activity) {
AsyncData(:final value) => Text('Activity: ${value.activity}'),
AsyncError() => const Text('Oops, something unexpected happened'),
_ => const CircularProgressIndicator(),
},
);
},
);
}
}
在該程式碼段中,我們使用了 Consumer
來讀取和 activityProvider
顯示活動。
我們還優雅地處理了載入/錯誤狀態。
請注意 UI 如何能夠處理載入/錯誤狀態,而無需在提供者程式中執行任何特殊操作。
同時,如果小部件要重建,則不會正確地重新執行網路請求。
其他小部件也可以訪問同一提供者程式,而無需重新執行網路請求。
小部件可以根據需要,監聽任意數量的提供者程式。為此,只需新增更多 ref.watch 呼叫即可。
確保安裝 linter。這將使您的 IDE 能夠提供重構選項,
以自動新增 Consumer
或將 StatelessWidget
重構為 ConsumerWidget
。
有關安裝步驟,請參閱入門指南。
更進一步:使用 ConsumerWidget
替代 Consumer
刪除程式碼縮排。
在前面的示例中,我們使用 Consumer
來讀取提供者程式。
儘管這種方法沒有錯,但新增的縮排會使程式碼更難閱讀。
Riverpod 提供了另一種實現相同結果的方法:
我們可以定義 ConsumerWidget
/ ConsumerStatefulWidget
來代替在
StatelessWidget
/ StatefulWidget
返回 Consumer
小元件。
ConsumerWidget
和 ConsumerStatefulWidget
實際上是 StatelessWidget
/ StatefulWidget
和 Consumer
的融合。
它們的行為與原來的 couterpart 相同,但具有提供“ref”的額外好處。
我們可以使用 ConsumerWidget
重寫前面的例子,如下所示:
/// 我們將“ConsumerWidget”替代“StatelessWidget”進行子類化。
/// 這相當於使用“StatelessWidget”並返回“Consumer”小元件。
class Home extends ConsumerWidget {
const Home({super.key});
// 請注意“build”現在如何接收一個額外的引數:“ref”
Widget build(BuildContext context, WidgetRef ref) {
// 我們可以像使用“Consumer”一樣,在小部件中使用“ref.watch”
final AsyncValue<Activity> activity = ref.watch(activityProvider);
// 渲染邏輯保持不變
return Center(/* ... */);
}
}
至於 ConsumerStatefulWidget
,我們會這樣寫:
// 我們擴充套件了 ConsumerStatefulWidget。
// 這等效於 "Consumer" + "StatefulWidget".
class Home extends ConsumerStatefulWidget {
const Home({super.key});
ConsumerState<ConsumerStatefulWidget> createState() => _HomeState();
}
// 請注意,我們如何擴充套件“ConsumerState”而不是“State”。
// 這和 "ConsumerWidget" 與 "StatelessWidget" 是相同的原理。
class _HomeState extends ConsumerState<Home> {
void initState() {
super.initState();
// 狀態生命週期也可以訪問“ref”。
// 這使得在特定提供者程式上新增監聽器,以便實現顯示對話方塊/資訊欄等功能。
ref.listenManual(activityProvider, (previous, next) {
// TODO 顯示一個 snackbar/dialog
});
}
Widget build(BuildContext context) {
// "ref" is not passed as parameter anymore, but is instead a property of "ConsumerState".
// We can therefore keep using "ref.watch" inside "build".
// “ref”不再作為引數傳遞,而是作為“ConsumerState”的屬性。
// 因此,我們可以繼續在“build”中使用“ref.watch”。
final AsyncValue<Activity> activity = ref.watch(activityProvider);
return Center(/* ... */);
}
}
Flutter_hooks 注意事項:結合 HookWidget
和 ConsumerWidget
如果您以前從未聽說過“鉤子(hooks)”,請隨時跳過本節。
Flutter_hooks 是一個獨立於 Riverpod 的軟體包,
但經常與 Riverpod 一起使用。如果您不熟悉 Riverpod,不建議使用“鉤子”。
有關詳細資訊,請參閱關於 Hooks(鉤子)。
如果您正在使用 flutter_hooks
,您可能想知道如何將 ConsumerWidget
和 HookWidget
組合在一起。畢竟,兩者都涉及更改擴充套件的小部件類。
Riverpod 為此問題提供瞭解決方案:HookConsumerWidget
和 StatefulHookConsumerWidget
。
類似於 ConsumerWidget
和 ConsumerStatefulWidget
是 StatelessWidget
/ StatefulWidget
和 Consumer
融合,
HookConsumerWidget
和 StatefulHookConsumerWidget
是 HookWidget
/ HookStatefulWidget
和 Consumer
的融合。
因此,它們允許在同一個小部件中同時使用鉤子和提供者程式。
為了展示這一點,我們可以再次重寫前面的例子:
/// 我們子類化了 "HookConsumerWidget"。
/// 這同時組合了 "StatelessWidget"、"Consumer"、"HookWidget"。
class Home extends HookConsumerWidget {
const Home({super.key});
// 請注意“build”現在如何接收一個額外的引數:“ref”
Widget build(BuildContext context, WidgetRef ref) {
// 可以在我們的小部件中使用諸如“useState”之類的鉤子
final counter = useState(0);
// 我們還可以使用讀取提供者程式
final AsyncValue<Activity> activity = ref.watch(activityProvider);
return Center(/* ... */);
}
}