在应用开发中,下拉刷新是一个常见需求,但处理起来往往比看起来要复杂。我们需要兼顾以下几种状态的无缝切换:
- 首次进入页面:显示加载指示器(Spinner)。
- 下拉刷新时:显示下拉刷新控件,同时保持当前数据内容不消失(避免屏幕闪烁)。
- 加载错误:优雅地展示错误信息。
- 刷新结束:隐藏刷新控件并更新数据。
Riverpod 的声明式特性结合 Dart 3 的模式匹配,可以非常简洁地解决这些问题。
1. 准备工作:定义数据模型与 Provider
首先,我们需要定义数据模型以及获取数据的 Provider。这里我们使用 riverpod_generator 和 freezed 来生成类型安全的代码。
1.1 定义数据模型 (Activity)
我们将构建一个简单的应用,从 API 获取随机活动建议。使用 Freezed 来处理不可变对象和 JSON 序列化。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'codegen.freezed.dart';
part 'codegen.g.dart';
@freezed
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);
}
1.2 创建 API Provider
使用 @riverpod 注解创建一个异步 Provider。该 Provider 负责发起网络请求并返回 Activity 对象。
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
// 生成的 Provider 代码将位于此处
part 'codegen.g.dart';
@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<String, dynamic>;
return Activity.fromJson(json);
}
2. 核心实现:构建 UI 与刷新逻辑
接下来是 UI 部分的实现。我们需要使用 Flutter 原生的 RefreshIndicator 组件,并结合 Riverpod 的 ref.watch 和 ref.refresh。
2.1 使用 RefreshIndicator
RefreshIndicator 需要一个可滚动的子组件(如 ListView)。它的 onRefresh 回调必须返回一个 Future,当这个 Future 完成时,刷新指示器才会消失。
2.2 触发刷新 (The Refresh Logic)
这是实现的关键:
- 不要手动维护一个
isLoading变量。 - 使用
ref.refresh(provider.future)。
ref.refresh(activityProvider) 会立即使 Provider 失效并重新计算,这通常会返回一个新的状态。但是,RefreshIndicator 需要等待刷新完成。因此,我们需要读取 .future 属性:
onRefresh: () => ref.refresh(activityProvider.future),
这将返回一个新的 Future,直到数据重新加载完毕后才会 resolve,从而完美控制刷新动画的结束时间。
2.3 智能处理加载状态 (Dart 3 模式匹配)
为了达到“刷新时保留旧数据,首次加载显示 Spinner”的效果,我们利用 AsyncValue 结合 Dart 3 的 switch 表达式。
关键点在于模式匹配的顺序:
- 优先匹配数据 (
:final value?):如果AsyncValue中包含数据(无论是当前数据还是刷新前的旧数据),就显示内容。这意味着即使在刷新过程中(状态变为 loading),只要之前有数据,用户依然能看到内容,而不是白屏。 - 匹配错误 (
:final error?):如果发生错误且没有旧数据,显示错误信息。 - 默认情况 (
_):既无数据也无错误(即首次加载),显示加载圈。
3. 完整代码示例
以下是整合了上述所有概念的完整代码(基于 riverpod_generator):
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';
// 注意:实际开发中需运行 build_runner 生成这两个文件
part 'main.g.dart';
part 'main.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) {
// 1. 监听 Provider 状态
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
// 2. 刷新逻辑:刷新 Provider 并等待 Future 完成
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
// 3. UI 状态渲染:使用 Dart 3 模式匹配
switch (activity) {
// 只要有数据(包括刷新期间),就显示数据
AsyncValue<Activity>(:final value?) => Padding(
padding: const EdgeInsets.all(16.0),
child: Text(value.activity),
),
// 有错误,显示错误信息
AsyncValue(:final error?) => Text('Error: $error'),
// 既无数据也无错误(初始加载),显示 Loading
_ => const Center(child: CircularProgressIndicator()),
},
],
),
),
);
}
}
// ================= 数据与 Provider 定义 =================
@riverpod
Future<Activity> activity(ActivityRef ref) async {
// 模拟 API 请求
final response = await http.get(
Uri.https('bored-api.appbrewery.com', '/random'),
);
// 解析 JSON
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Activity.fromJson(json);
}
@freezed
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);
}
4. 关键点总结
ref.refresh(provider.future)是核心: 它不仅触发重新获取数据,还返回一个 Future,让RefreshIndicator知道何时停止旋转。
- UI 渲染逻辑的健壮性: 使用
AsyncValue<Activity>(:final value?)能够正确处理“正在刷新”的状态。Riverpod 的AsyncValue在刷新时会保留上一次的数据(isLoading: true但value不为空),我们可以利用这一点防止页面在刷新时闪烁回 Loading 状态。
- 代码生成: 使用
riverpod_generator大幅减少了样板代码,使 Provider 的定义更加清晰易读。