下拉重新整理
由於其宣告性,Riverpod 本身就支援拉動重新整理。
一般來說,拉動重新整理可能很複雜,因為有多個問題需要解決:
- 第一次進入頁面時,我們想要顯示一個微調器(spinner)。 但在重重新整理期間,我們希望顯示重新整理指示器。 我們不應該同時顯示重新整理指示器和微調器。
- 當重新整理掛起時,我們希望顯示以前的資料/錯誤。
- 只要重刷新發生,我們就需要顯示重新整理指示器。
讓我們看看如何使用 Riverpod 解決這個問題。
為此,我們將製作一個簡單的示例,向用戶推薦隨機活動。
並且進行下拉重新整理將觸發新的建議:

製作一個簡單的應用程式。
在實現下拉重新整理之前,我們首先需要重新整理一些東西。
我們可以製作一個簡單的應用程式,使用 Bored API
向用戶建議隨機活動。
首先,我們定義一個 Activity
類:
sealed class Activity with _$Activity {
factory Activity({
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;
factory Activity.fromJson(Map<String, dynamic> json) =>
_$ActivityFromJson(json);
}
該類將負責以型別安全的方式表示建議的活動,並處理 JSON 編碼/解碼。
使用 Freezed/json_serialized 不是必需的,但建議使用。
現在,我們要定義一個提供者程式發出 HTTP GET 請求來獲取單個活動:
Future<Activity> activity(Ref ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(Map.from(json));
}
我們現在可以使用此提供者程式來顯示隨機活動。
目前,我們不會處理載入/錯誤狀態,而只是在可用時顯示活動:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: Center(
// If we have an activity, display it, otherwise wait
child: Text(activity.valueOrNull?.activity ?? ''),
),
);
}
}
新增 RefreshIndicator
現在我們有了一個簡單的應用程式,我們可以向它新增一個 RefreshIndicator
。
該小部件是一個官方的 Material 小部件,負責在使用者下拉螢幕時顯示重新整理指示器。
使用 RefreshIndicator
需要一個可滾動的表面。但到目前為止,我們還沒有。
我們可以透過使用 ListView
/GridView
/SingleChildScrollView
等等來解決這個問題:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () async => print('refresh'),
child: ListView(
children: [
Text(activity.valueOrNull?.activity ?? ''),
],
),
),
);
}
}
使用者現在可以下拉螢幕。但我們的資料還沒有重新整理。
新增重新整理邏輯
當用戶下拉螢幕時,RefreshIndicator
將呼叫
onRefresh
回撥。我們可以使用該回調來重新整理我們的資料。
在那裡,我們可以使用 ref.refresh
重新整理我們選擇的提供者程式。
注意:onRefresh
期望返回一個 Future
。
重新整理完成後,future 的完成非常重要。
為了獲得這樣的 future,我們可以讀取提供者程式的 .future
屬性。
這將返回一個 future,該 future 在我們的提供者程式解決後完成。
因此,我們可以將 RefreshIndicator
更新為如下所示:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
// By refreshing "activityProvider.future", and returning that result,
// the refresh indicator will keep showing until the new activity is
// fetched.
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
Text(activity.valueOrNull?.activity ?? ''),
],
),
),
);
}
}
僅在初始載入和處理錯誤期間顯示微調器。
目前,我們的 UI 不處理錯誤/載入狀態。
相反,當載入/重新整理完成時,資料會神奇地彈出。
讓我們透過優雅地處理這些狀態來改變這一點。有兩種情況:
- 在初始載入期間,我們希望顯示全屏微調器。
- 在重新整理期間,我們希望顯示重新整理指示器和之前的資料/錯誤。
幸運的是,當在 Riverpod 中監聽非同步提供者程式時,
Riverpod 為我們提供了一個 AsyncValue
,它提供了我們需要的一切。
然後可以將 AsyncValue
與 Dart 3.0 的模式匹配結合起來,如下所示:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
switch (activity) {
// If some data is available, we display it.
// Note that data will still be available during a refresh.
AsyncValue<Activity>(:final valueOrNull?) => Text(valueOrNull.activity),
// An error is available, so we render it.
AsyncValue(:final error?) => Text('Error: $error'),
// No data/error, so we're in loading state.
_ => const CircularProgressIndicator(),
},
],
),
),
);
}
}
我們在這裡使用 valueOrNull
,就像目前一樣,
如果處於錯誤/載入狀態,則使用 value
會丟擲異常。
Riverpod 3.0 將對此進行更改,使 value
的行為類似於 valueOrNull
。
但現在,讓我們堅持使用 valueOrNull
。
請注意我們的模式匹配中 :final valueOrNull?
語法的使用。
只能使用此語法,因為 activityProvider
返回不可為 null 的 Activity
。
如果您的資料可以是 null
,則可以使用 AsyncValue(hasData: true, :final valueOrNull)
。
這將正確處理資料為 null
的情況,但需要一些額外的字元。
總結:完整的應用
以下是組合了我們迄今為止所涵蓋的所有內容的原始碼:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'codegen.g.dart';
part 'codegen.freezed.dart';
void main() => runApp(ProviderScope(child: MyApp()));
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(home: ActivityView());
}
}
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
switch (activity) {
AsyncValue<Activity>(:final valueOrNull?) =>
Text(valueOrNull.activity),
AsyncValue(:final error?) => Text('Error: $error'),
_ => const CircularProgressIndicator(),
},
],
),
),
);
}
}
Future<Activity> activity(Ref ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(Map.from(json));
}
sealed class Activity with _$Activity {
factory Activity({
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;
factory Activity.fromJson(Map<String, dynamic> json) =>
_$ActivityFromJson(json);
}