在应用开发中,尤其是涉及网络请求(如搜索、分页加载)的场景下,我们经常面临一个问题:当用户频繁改变输入或离开当前页面时,之前的请求可能还在后台运行。 这不仅浪费流量和资源,还可能导致“竞态条件”(旧数据的返回覆盖了新数据)。

Riverpod 提供了一套内置的生命周期机制,可以轻松解决这个问题。本文将从自动取消防抖(Debouncing)两个方面进行讲解。

1. 核心机制:ref.onDispose

Riverpod 的核心理念之一是:当 Provider 不再被使用,或由于参数变化(如 .family)导致重新计算时,旧的 Provider 状态会被销毁(Disposed)。

我们可以利用 ref.onDispose 注册一个回调函数,当 Provider 被销毁时,执行清理逻辑(如关闭 HTTP 客户端、取消 Token 等)。

为什么这很重要?

假设用户正在搜索框输入内容:

  1. 用户输入 "A",触发请求 A。
  2. 用户紧接着输入 "B"(变成 "AB"),触发请求 B。
  3. 此时,请求 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'];
}

工作流程:

  1. 如果 UI 不再监听这个 Provider(组件卸载),client.close() 被调用,请求终止。
  2. 如果这是一个带有参数的 family provider,当参数变化时,旧的 provider 实例会被销毁,触发 client.close(),同时新的 provider 实例启动新请求。

3. 进阶技巧:实现“防抖”(Debouncing)

在搜索场景中,我们不希望用户每敲击一次键盘就发一次请求。通常的做法是等待用户停止输入一段时间(例如 500ms)后再发起请求。

在 Riverpod 中,我们不需要引入额外的防抖库,结合 Future.delayedref.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();
}

逻辑解析

  1. 用户输入 "ap" -> 创建 Provider 实例 1 -> 开始计时 500ms。
  2. 200ms 后,用户输入 "pl" (变成 "appl") -> 参数变化,Riverpod 销毁 实例 1,创建 实例 2。
  3. 实例 1ref.onDispose 触发,didDispose 变为 true
  4. 实例 1 的 500ms 计时结束,检查 if (didDispose),条件成立,抛出异常,此时网络请求从未发出
  5. 实例 2 如果在其 500ms 计时结束前没有新输入,则会顺利通过检查,发起网络请求。

4. 总结

在 Riverpod 中处理请求取消并非难事,主要依赖于 ref 提供的生命周期钩子。

  • 基本原则:始终在 ref.onDispose 中清理资源(关闭 Client、取消 Token 等)。
  • 防抖实现:利用 await Future.delayed 配合“是否已销毁”的检查,可以轻松实现“搜索即时响应”(Search-as-you-type)功能,既节省了服务器资源,又提升了用户体验。

通过这种方式,你的应用将变得更加健壮,不再受制于过时数据的干扰。