在应用开发中,尤其是涉及网络请求(如搜索、分页加载)的场景下,我们经常面临一个问题:当用户频繁改变输入或离开当前页面时,之前的请求可能还在后台运行。 这不仅浪费流量和资源,还可能导致“竞态条件”(旧数据的返回覆盖了新数据)。
Riverpod 提供了一套内置的生命周期机制,可以轻松解决这个问题。本文将从自动取消和防抖(Debouncing)两个方面进行讲解。
1. 核心机制:ref.onDispose
Riverpod 的核心理念之一是:当 Provider 不再被使用,或由于参数变化(如 .family)导致重新计算时,旧的 Provider 状态会被销毁(Disposed)。
我们可以利用 ref.onDispose 注册一个回调函数,当 Provider 被销毁时,执行清理逻辑(如关闭 HTTP 客户端、取消 Token 等)。
为什么这很重要?
假设用户正在搜索框输入内容:
- 用户输入 "A",触发请求 A。
- 用户紧接着输入 "B"(变成 "AB"),触发请求 B。
- 此时,请求 A 的结果已经不再需要了。如果不取消,它不仅占用带宽,其返回结果甚至可能错误地覆盖请求 B 的结果。
2. 基础实现:取消 HTTP 请求
下面是一个使用 http 包的示例。我们将创建一个 HTTP 客户端,并在 Provider 销毁时关闭它。
代码示例
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'provider.g.dart';
@riverpod
Future<String> fetchUserData(FetchUserDataRef ref) async {
// 1. 创建 http 客户端
final client = http.Client();
// 2. 注册销毁回调:一旦 Provider 被销毁(例如用户离开了页面或重新刷新),
// 立即关闭客户端,这会中止挂起的请求。
ref.onDispose(() {
client.close();
});
// 3. 使用该客户端发起请求
final response = await client.get(
Uri.parse('https://your-api.com/user'),
);
return jsonDecode(response.body)['name'];
}
工作流程:
- 如果 UI 不再监听这个 Provider(组件卸载),
client.close()被调用,请求终止。 - 如果这是一个带有参数的 family provider,当参数变化时,旧的 provider 实例会被销毁,触发
client.close(),同时新的 provider 实例启动新请求。
3. 进阶技巧:实现“防抖”(Debouncing)
在搜索场景中,我们不希望用户每敲击一次键盘就发一次请求。通常的做法是等待用户停止输入一段时间(例如 500ms)后再发起请求。
在 Riverpod 中,我们不需要引入额外的防抖库,结合 Future.delayed 和 ref.onDispose 即可实现。
代码示例
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search.g.dart';
@riverpod
Future<List<String>> search(SearchRef ref, String query) async {
// --- 防抖逻辑开始 ---
// 1. 标志位:记录该 Provider 是否已被销毁
var didDispose = false;
ref.onDispose(() => didDispose = true);
// 2. 延迟执行:等待 500 毫秒
await Future.delayed(const Duration(milliseconds: 500));
// 3. 检查状态:如果在这 500ms 内用户又输入了新字符,
// Riverpod 会销毁当前的 Provider 实例(触发 onDispose),
// 并创建一个新的实例。
if (didDispose) {
// 如果已销毁,抛出异常以中断后续逻辑
throw Exception('Cancelled');
}
// --- 防抖逻辑结束,开始实际请求 ---
final client = http.Client();
ref.onDispose(client.close); // 再次注册,用于处理请求期间的取消
final response = await client.get(
Uri.parse('https://api.example.com/search?q=$query'),
);
final List data = jsonDecode(response.body);
return data.map((e) => e.toString()).toList();
}
逻辑解析
- 用户输入 "ap" -> 创建 Provider 实例 1 -> 开始计时 500ms。
- 200ms 后,用户输入 "pl" (变成 "appl") -> 参数变化,Riverpod 销毁 实例 1,创建 实例 2。
- 实例 1 的
ref.onDispose触发,didDispose变为true。 - 实例 1 的 500ms 计时结束,检查
if (didDispose),条件成立,抛出异常,此时网络请求从未发出。 - 实例 2 如果在其 500ms 计时结束前没有新输入,则会顺利通过检查,发起网络请求。
4. 总结
在 Riverpod 中处理请求取消并非难事,主要依赖于 ref 提供的生命周期钩子。
- 基本原则:始终在
ref.onDispose中清理资源(关闭 Client、取消 Token 等)。 - 防抖实现:利用
await Future.delayed配合“是否已销毁”的检查,可以轻松实现“搜索即时响应”(Search-as-you-type)功能,既节省了服务器资源,又提升了用户体验。
通过这种方式,你的应用将变得更加健壮,不再受制于过时数据的干扰。