跳到主要內容

下拉重新整理

由於其宣告性,Riverpod 本身就支援拉動重新整理。

一般來說,拉動重新整理可能很複雜,因為有多個問題需要解決:

  • 第一次進入頁面時,我們想要顯示一個微調器(spinner)。 但在重重新整理期間,我們希望顯示重新整理指示器。 我們不應該同時顯示重新整理指示器微調器。
  • 當重新整理掛起時,我們希望顯示以前的資料/錯誤。
  • 只要重刷新發生,我們就需要顯示重新整理指示器。

讓我們看看如何使用 Riverpod 解決這個問題。
為此,我們將製作一個簡單的示例,向用戶推薦隨機活動。
並且進行下拉重新整理將觸發新的建議:

上面描述的應用軟體工作時的 gif

製作一個簡單的應用程式。​

在實現下拉重新整理之前,我們首先需要重新整理一些東西。
我們可以製作一個簡單的應用程式,使用 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);
}