原文链接:Pull to refresh | Riverpod
pub:riverpod | Dart Package (flutter-io.cn)
译时版本: 2.4.9
之前翻译过 Riverpod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。
之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)
下拉刷新
Riverpod 得益于其声明式的属性天生支持下拉刷新。
通常,下拉刷新都很复杂,因其需要解决多个问题:
- 第一次进入某个画面时,想要表示一个加载中的指示器。但是在刷新时,是想要表示刷新指示器。不应该同时表示刷新指示器和加载中的指示器。
- 还没开始刷新时,需要表示之前的数据/错误。
- 需要在刷新过程中一直表示刷新指示器。
看一下如何用 Riverpod解决这个问题。
对于该问题,会创建一个简单示例,该示例会向用户推荐一个随机 activity 。
下拉刷新会触发一个新的建议:
创建一个应用的基本框架
实现下拉刷新之前,首先还需要一些准备用于刷新的内容。
可以创建一个简单的应用,该应用使用 Bored API 向用户推荐一个随机 activity 。
首先,定义 Activity 类:
@freezed
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);
}
该类用于表现建议的activity,它是以类型安全的方式,并处理 JSON 编解码。
使用 Freezed/json_serializable 不是必须的,但是推荐这么做。
现在,是要定义一个 provider ,以创建 HTTP GET 请求获取单个 activity:
@riverpod
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));
}
现在要用这个 provider 来显示一个随机 activity 。
现在还不处理加载中/错误状态,只是简单地在 activity 可用时将其显示:
class ActivityView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: Center(
// 如果有 activity 就显示,否则继续等待。
child: Text(activity.valueOrNull?.activity ?? ''),
),
);
}
}
添加 RefreshIndicator (刷新指示器)
现在有了一个简单的应用,可以向其添加 RefreshIndicator (刷新指示器)。
该组件是官方的 Material 组件,用于在用户下拉屏幕时显示一个刷新的指示器。
使用 RefreshIndicator 需要一个可滚动界面。但是至今还没有。可以使用 ListView/GridView/SingleChildScrollView 等来修改:
class ActivityView extends ConsumerWidget {
@override
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 来刷新所选择的 provider 。
注意: onRefresh 的期待值是返回一个 Future 。所以 future 在刷新结束时完成是很重要的.
要获得这样的特性,可以读取 provider 的 .future 属性。
这会返回一个 future ,在 provider 解决时,该 future 将会完成。
因此就能如下更新 RefreshIndicator 了:
class ActivityView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
// 刷新 "activityProvider.future" ,返回其结果,
// 刷新指示器会一直表示直到获取到新的 activity 。
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
Text(activity.valueOrNull?.activity ?? ''),
],
),
),
);
}
}
只在初始加载和处理错误时表示加载指示器
此时,UI不会处理错误/加载中状态。
加载中/刷新完成时数据会魔法般出现。
修改为优雅地处理这些状态。有两种情况:
- 在初始化加载时,想表示一个全屏的指示器。
- 刷新时,想表示刷新指示器和之前的数据/错误。
幸运的是,在 Riverpod 中监听一个异步的 provider 时,Riverpod 会给予一个 AsyncValue ,它提供了所需的所有内容。
该 AsyncValue 之后可如下用 Dart 3.0 的模式匹配进行绑定:
class ActivityView extends ConsumerWidget {
@override
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(),
},
],
),
),
);
}
}
警告
这里使用
valueOrNull,现在使用value如果是错误/加载中状态,会抛出异常。Riverpod 3.0 会修改为
value,其行为像valueOrNull。 但是现在,先继续使用valueOrNull。
提示
注意模式匹配中
:final valueOrNull?语法的用法。该语法仅在activityProvider返回一个不为空的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 {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ActivityView());
}
}
class ActivityView extends ConsumerWidget {
@override
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(),
},
],
),
),
);
}
}
@riverpod
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));
}
@freezed
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);
}