Files
jiu/client/lib/providers/connectivity_provider.dart
T
wangjia 018180de8c feat(client): 网络请求重试 + 离线提示可感知重试按钮
修复①请求级重试:新增 RetryInterceptor,网络层失败重试 3 次间隔递增
1s→2s→4s;GET 全重试,写操作仅在连接未建立时重试以防重复提交。
connectTimeout 5s→8s。

修复②连通性:启动首检改退避重试 4s→8s→12s,根治跨境冷启动假离线;
新增检测中状态 + retry();登录页/全局离线提示加 NetworkRetryButton,
重试中转圈、结果 SnackBar 反馈。

121 个测试通过,analyze 无 error。todo #52。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:42:23 +08:00

130 lines
4.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/config/app_config.dart';
/// 每次从离线恢复到在线时自增。
/// 数据 provider 通过 watch 此值实现网络恢复后自动刷新。
final networkRecoveryCountProvider = StateProvider<int>((ref) => 0);
/// 是否正在进行一次「带重试的连通性检测」(供 UI 显示「重试中…」转圈)。
final connectivityCheckingProvider = StateProvider<bool>((ref) => false);
final connectivityProvider =
StateNotifierProvider<ConnectivityNotifier, bool>((ref) {
return ConnectivityNotifier(
onRecovered: () {
ref.read(networkRecoveryCountProvider.notifier).update((s) => s + 1);
},
onCheckingChanged: (checking) {
ref.read(connectivityCheckingProvider.notifier).state = checking;
},
);
});
class ConnectivityNotifier extends StateNotifier<bool> {
final void Function()? onRecovered;
final void Function(bool checking)? onCheckingChanged;
ConnectivityNotifier({
this.onRecovered,
this.onCheckingChanged,
bool skipInit = false,
}) : super(true) {
if (!skipInit) {
// 启动首检走「带重试」:跨境冷启动 DNS+TLS 握手较慢,单次 4s 易误判离线,
// 退避重试 4s→8s→12s 可显著降低首屏假离线。
_check(withRetry: true);
_startOnlineTimer();
}
}
Timer? _timer;
// 独立轻量 Dio:无拦截器。连接预算用 Future.timeout 逐次控制,
// 故 BaseOptions 超时放宽到 15s 作为兜底。
final _dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
));
/// 每次尝试的超时预算,逐次拉长(越来越长)。
static const _budgets = <Duration>[
Duration(seconds: 4),
Duration(seconds: 8),
Duration(seconds: 12),
];
/// 在线时:每 30 秒轻量检测一次(单次,不重试)
void _startOnlineTimer() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 30), (_) => _check());
}
/// 离线时:每 1 分钟嗅探一次,降低频率节省资源
void _startOfflineTimer() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(minutes: 1), (_) => _check());
}
/// 立即触发一次检测(供外部调用,如 API 请求失败 / 启动时)。单次,不重试。
Future<void> forceCheck() => _check();
/// 用户手动重试:带退避重试,并对外广播「检测中」状态,返回最终是否在线。
Future<bool> retry() async {
onCheckingChanged?.call(true);
try {
return await _check(withRetry: true);
} finally {
onCheckingChanged?.call(false);
}
}
/// 单次 ping /health[budget] 为本次超时预算。
Future<bool> _ping(Duration budget) async {
try {
await _dio.get(AppConfig.healthUrl).timeout(budget);
return true;
} catch (_) {
return false;
}
}
/// 连通性检测。[withRetry]=true 时按 [_budgets] 退避重试,间隔递增。
Future<bool> _check({bool withRetry = false}) async {
final budgets = withRetry ? _budgets : const [Duration(seconds: 4)];
var ok = false;
for (var i = 0; i < budgets.length; i++) {
ok = await _ping(budgets[i]);
if (ok) break;
// 最后一次失败后不再等待;中间失败按递增间隔退避(0.6s, 1.2s…)
if (i < budgets.length - 1) {
await Future.delayed(Duration(milliseconds: 600 * (i + 1)));
}
}
if (ok) {
if (!state) {
// 离线 → 在线:切回高频检测,广播恢复事件
state = true;
onRecovered?.call();
_startOnlineTimer();
}
} else {
if (state) {
// 在线 → 离线:切换为低频嗅探
state = false;
_startOfflineTimer();
}
}
return ok;
}
@override
void dispose() {
_timer?.cancel();
_dio.close(force: true);
super.dispose();
}
}