下拉刷新
由于其声明性,Riverpod 本身就支持拉动刷新。
一般来说,拉动刷新可能很复杂,因为有多个问题需要解决:
- 第一次进入页面时,我们想要显示一个微调器(spinner)。 但在重刷新期间,我们希望显示刷新指示器。 我们不应该同时显示刷新指示器和微调器。
- 当刷新挂起时,我们希望显示以前的数据/错误。
- 只要重刷新发生,我们就需要显示刷新指示器。
让我们看看如何使用 Riverpod 解决这个问题。
为此,我们将制作一个简单的示例,向用户推荐随机活动。
并且进行下拉刷新将触发新的建议:
制作一个简单的应用程序。
在实现下拉刷新之前,我们首先需要刷新一些东西。
我们可以制作一个简单的应用程序,使用 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(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));
}
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);
}