Flutter Riverpod 3:统一打印 Provider 错误与自动重试日志

Flutter Riverpod 3:统一打印 Provider 错误与自动重试日志

在使用 FutureProvider 请求接口时,网络异常会让 Provider 进入错误状态。

Riverpod 3 还会对部分异常执行自动重试。

问题在于:如果项目没有统一日志,很容易只看到页面一直加载,却不知道 Provider 抛了什么异常,也不知道它重试了几次。

例如:

ini 复制代码
final musicHomeProvider = FutureProvider.autoDispose(
  (ref) => ref.watch(musicHomeRepositoryProvider).fetchHome(),
);

当 fetchHome() 因网络断开抛出异常时,希望日志能看到两件事:

ini 复制代码
Provider Error
DioException: connection error

Provider Retry
count=1
delay=200ms

继续失败:

ini 复制代码
Provider Error
DioException: connection error

Provider Retry
count=2
delay=400ms

这类日志可以放到全局,不需要给每个 Provider 写 try/catch。

1. 使用 ProviderObserver 打印错误

先定义一个全局 ProviderObserver:

scala 复制代码
class AppProviderObserver extends ProviderObserver {
  @override
  void providerDidFail(
    ProviderObserverContext context,
    Object error,
    StackTrace stackTrace,
  ) {
    if (error is ProviderException) return;

    debugPrint(
      '[Provider Error] '
      '${context.provider.name ?? context.provider.runtimeType}\n'
      '$error',
    );

    debugPrintStack(
      stackTrace: stackTrace,
    );
  }
}

当 Provider 执行过程中抛出异常时,providerDidFail 会收到:

go 复制代码
error
stackTrace

这样可以保留完整错误栈。

这里额外过滤:

kotlin 复制代码
if (error is ProviderException) return;

目的是避免依赖链失败时产生重复日志。

例如:

markdown 复制代码
repositoryProvider
    ↓
musicHomeProvider

底层 Provider 已经记录一次异常,上层 Provider 可能继续收到依赖失败。

过滤 ProviderException 后,日志会干净一些。

2. 打印 Riverpod 自动重试日志

错误日志只能说明 Provider 失败了。

如果 Riverpod 执行自动重试,还需要把重试过程打印出来。

可以在 ProviderScope 配置全局 retry:

less 复制代码
void main() {
  runApp(
    ProviderScope(
      observers: [
        AppProviderObserver(),
      ],
      retry: (retryCount, error) {
        final delay = ProviderContainer.defaultRetry(
          retryCount,
          error,
        );

        debugPrint(
          '[Provider Retry] '
          'count=${retryCount + 1}, '
          'delay=$delay, '
          'error=$error',
        );

        return delay;
      },
      child: const App(),
    ),
  );
}

这里没有自己修改重试规则,而是继续调用:

go 复制代码
ProviderContainer.defaultRetry(
  retryCount,
  error,
);

它返回 Riverpod 默认的重试时间。

我们只在中间插入一条日志。

3. Provider 不需要修改

完成全局配置后,业务 Provider 保持原样:

ini 复制代码
final musicHomeProvider = FutureProvider.autoDispose(
  (ref) => ref.watch(musicHomeRepositoryProvider).fetchHome(),
);

不需要写成:

dart 复制代码
final musicHomeProvider = FutureProvider.autoDispose((ref) async {
  try {
    return await ref.watch(
      musicHomeRepositoryProvider,
    ).fetchHome();
  } catch (e, stackTrace) {
    debugPrint('$e');
    debugPrintStack(stackTrace: stackTrace);
    rethrow;
  }
});

否则每个 Provider 都要重复一遍日志代码。

Provider 多起来以后,这种写法会散落在整个项目里。

4. 实际日志效果

例如设备断网:

css 复制代码
[Provider Error] musicHomeProvider
DioException [connection error]:
The connection errored

Riverpod 开始重试:

ini 复制代码
[Provider Retry]
count=1,
delay=0:00:00.200000,
error=DioException [connection error]

再次失败:

css 复制代码
[Provider Error] musicHomeProvider
DioException [connection error]:
The connection errored

继续重试:

ini 复制代码
[Provider Retry]
count=2,
delay=0:00:00.400000,
error=DioException [connection error]

这样调试网络问题时,可以直接看出:

javascript 复制代码
请求
↓
异常
↓
Provider Error
↓
Riverpod Retry
↓
重新请求
↓
再次异常

5. 完整代码

最后可以收敛成下面这套配置:

scala 复制代码
class AppProviderObserver extends ProviderObserver {
  @override
  void providerDidFail(
    ProviderObserverContext context,
    Object error,
    StackTrace stackTrace,
  ) {
    if (error is ProviderException) return;

    debugPrint(
      '[Provider Error] '
      '${context.provider.name ?? context.provider.runtimeType}\n'
      '$error',
    );

    debugPrintStack(
      stackTrace: stackTrace,
    );
  }
}

然后:

less 复制代码
void main() {
  runApp(
    ProviderScope(
      observers: [
        AppProviderObserver(),
      ],
      retry: (retryCount, error) {
        final delay = ProviderContainer.defaultRetry(
          retryCount,
          error,
        );

        debugPrint(
          '[Provider Retry] '
          'count=${retryCount + 1}, '
          'delay=$delay, '
          'error=$error',
        );

        return delay;
      },
      child: const App(),
    ),
  );
}

业务层继续写普通 Provider:

ini 复制代码
final musicHomeProvider = FutureProvider.autoDispose(
  (ref) => ref.watch(musicHomeRepositoryProvider).fetchHome(),
);

错误由 ProviderObserver 记录。

重试由 ProviderScope.retry 记录。

业务代码不处理日志,也不吞异常。

相关推荐
周洲08301 小时前
STM32片内Flash读写深度详解|掉电参数保存、底层原理
linux·前端·stm32
data analyse 4561 小时前
埋点工具的私有化部署成本高吗?
前端·数据分析·github
data analyse 4562 小时前
埋点工具的埋点查询语言难学吗?
前端·数据分析
m0_738185822 小时前
Flutter 鸿蒙化实战:flutter_exif_plugin 适配 OpenHarmony,图片 EXIF 读写
flutter·华为·harmonyos·鸿蒙
DianSan_ERP2 小时前
多平台订单自动下载与回传的技术实现:从消息推送到状态闭环引言
java·linux·服务器·前端·网络·架构·自动化
恋猫de小郭2 小时前
Shopify 回应为什么从 RN 回到原生,为什么不用 KMP ?
android·前端·flutter
小蜗 strong3 小时前
和电脑猜拳(随机程序应用)
服务器·前端·python
下家4 小时前
为了脱离前端鄙视链,于是自己写个框架 - React 党看完沉默了
前端·前端框架
zhiyouTech4 小时前
从“被看见“到“被信任“:关于信源评级机制的一点观察
大数据·前端·人工智能