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 记录。
业务代码不处理日志,也不吞异常。