在现代应用开发中,网络请求失败是常见问题。Riverpod 引入了自动重试(Automatic Retry)机制,旨在让应用在面对暂时性错误(如网络波动)时更具弹性。

本文将系统地介绍 Riverpod 的自动重试行为、默认策略、以及如何通过 riverpod_generator 自定义或禁用该功能。

1. 核心概念:什么是自动重试?

当一个 Provider 在计算过程中抛出异常(例如 HTTP 请求失败)时,Riverpod 会自动尝试重新执行该 Provider 的构建逻辑,直到成功或达到最大重试次数。

这种机制可以显著简化错误处理代码,开发者无需手动编写复杂的递归重试逻辑。

2. 默认重试策略

Riverpod 提供了一个智能的默认重试策略,设计目标是既能恢复暂时性错误,又避免因频繁重试而导致资源浪费。

  • 重试次数:默认最多重试 10 次
  • 退避策略(Backoff):采用指数退避算法。
    • 初次重试延迟:200ms
    • 后续延迟:每次翻倍(200ms -> 400ms -> 800ms ...)。
    • 最大延迟:6.4 秒

注意

  • Error 类型不重试:如 OutOfMemoryError 或代码逻辑错误(Bug),这些被认为是不可恢复的,重试只会污染日志。
  • ProviderException 不重试:如果当前 Provider 失败是因为它依赖的上游 Provider 失败了(抛出了 ProviderException),则不会重试当前 Provider,而是应该重试那个上游 Provider。

3. 使用 riverpod_generator 自定义重试逻辑

虽然默认行为已经足够应对大多数场景,但你可能需要根据业务需求调整重试逻辑(例如减少重试次数,或在特定错误下停止重试)。

我们可以通过 @Riverpod 注解中的 retry 参数来实现。

3.1 定义自定义重试函数

重试逻辑是一个简单的函数,接收当前重试次数和错误对象,返回下一次重试前的等待时间(Duration?)。如果返回 null,则停止重试。

import 'package:riverpod_annotation/riverpod_annotation.dart';

// 自定义重试策略示例
Duration? myRetryPolicy(int retryCount, Object error) {
  // 1. 如果重试超过 3 次,停止重试
  if (retryCount >= 3) return null;

  // 2. 可以在这里判断 error 类型,例如如果是 "用户未授权" 错误,则不重试
  // if (error is UnauthorizedException) return null;

  // 3. 返回指数退避时间:200ms, 400ms, 800ms...
  return Duration(milliseconds: 200 * (1 << retryCount));
}

3.2 在 Provider 中应用

使用 riverpod_generator 时,直接将上述函数传递给注解即可。

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:dio/dio.dart';

part 'data_provider.g.dart';

// 将自定义策略应用到 Provider
@Riverpod(retry: myRetryPolicy)
Future<String> fetchData(FetchDataRef ref) async {
  // 模拟网络请求
  final response = await Dio().get('https://api.example.com/data');
  return response.data as String;
}

4. 全局配置与禁用

除了针对单个 Provider 设置,你也可以在应用的根节点对所有 Provider 生效,或者完全禁用重试。

4.1 全局设置 (在 ProviderScope 中)

如果你希望整个应用使用统一的重试策略,可以在 ProviderScope 中配置:

void main() {
  runApp(
    ProviderScope(
      // 对所有未显式指定 retry 的 Provider 生效
      retry: myRetryPolicy, 
      child: const MyApp(),
    ),
  );
}

4.2 禁用重试

如果你希望某个 Provider 失败后立即报错,不进行任何重试,可以让重试函数始终返回 null

禁用单个 Provider:

Duration? noRetry(int retryCount, Object error) => null;

@Riverpod(retry: noRetry)
Future<int> noRetryProvider(NoRetryProviderRef ref) async {
  throw Exception('Fail immediately');
}

全局禁用:

ProviderScope(
  retry: (retryCount, error) => null, // 全局禁用自动重试
  child: const MyApp(),
);

5. 进阶行为:Future 的等待机制

在使用 ref.watch(provider.future) 监听异步 Provider 时,Riverpod 的自动重试机制会影响 await 的行为。

  • 挂起等待:当 Provider 失败并开始重试时,await provider.future 不会立即抛出异常
  • 持续挂起:它会一直处于挂起状态(Pending),直到:
    1. 某次重试成功(返回数据)。
    2. 重试次数耗尽或策略返回 null(此时才抛出异常)。

这意味着在 UI 中使用 FutureBuilderawait 时,即使中间发生了一两次网络闪断,只要重试最终成功,用户端可能根本感知不到错误的发生。


总结

特性 说明
默认行为 自动重试,指数退避(200ms -> 6.4s),最多 10 次。
不可重试项 Error (代码 Bug) 和 ProviderException (上游依赖错误)。
自定义方式 通过 @Riverpod(retry: myPolicy) 指定。
generator 用法 传入一个返回 Duration? 的函数。
UI 表现 future 会等待重试完成,提供无缝体验。