跳到主要内容

下拉刷新

由于其声明性,Riverpod 本身就支持拉动刷新。

一般来说,拉动刷新可能很复杂,因为有多个问题需要解决:

  • 第一次进入页面时,我们想要显示一个微调器(spinner)。 但在重刷新期间,我们希望显示刷新指示器。 我们不应该同时显示刷新指示器微调器。
  • 当刷新挂起时,我们希望显示以前的数据/错误。
  • 只要重刷新发生,我们就需要显示刷新指示器。

让我们看看如何使用 Riverpod 解决这个问题。
为此,我们将制作一个简单的示例,向用户推荐随机活动。
并且进行下拉刷新将触发新的建议:

上面描述的应用软件工作时的 gif

制作一个简单的应用程序。​

在实现下拉刷新之前,我们首先需要刷新一些东西。
我们可以制作一个简单的应用程序,使用 Bored API 向用户建议随机活动。

首先,我们定义一个 Activity 类:


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(ActivityRef 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(ActivityRef 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 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);
}