Initial project

This commit is contained in:
Cody
2026-03-06 14:56:17 +08:00
parent 977b627b15
commit bf9e099747
1180 changed files with 50973 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
import 'package:flutter/widgets.dart';
/// App 启动初始化器
///
/// 两阶段串行队列,确保启动流畅、资源不竞争。
///
/// ## 为什么不能在 initState 里一股脑 await
///
/// 初始化任务并发执行会导致:
/// - 多个 await 阻塞首帧渲染 → 白屏时间长
/// - 任务间资源竞争网络、IO、CPU→ 互相拖慢
/// - 一个任务失败可能阻塞后续所有任务
///
/// ## 解决方案:两阶段 + 串行队列
///
/// ```
/// App 启动
/// │
/// ├── Phase 1: CriticalinitState 中同步触发)
/// │ 串行执行,必须在用户交互前完成。
/// │ 只放真正阻塞用户操作的任务(尽量少)。
/// │ 例:网络监听(后续所有网络操作依赖它)
/// │
/// ├── 首帧渲染(用户看到 UI
/// │
/// └── Phase 2: DeferredaddPostFrameCallback 触发)
/// 串行执行,首帧渲染后逐个跑。
/// 不争抢资源,不影响 UI 流畅度。
/// 例:推送注册、缓存预热、埋点 SDK 初始化
/// ```
///
/// ## 添加新任务的规则
///
/// 问自己:「这个任务不完成,用户能正常看到首页吗?」
/// - **能** → 放 deferred绝大多数情况
/// - **不能** → 放 critical谨慎添加每多一个都会拖慢启动
///
/// ## 设计原则
///
/// - **串行不并发**:同阶段内任务按顺序逐个执行,避免资源竞争
/// - **隔离不传染**:每个任务独立 try-catch一个失败不阻塞后续
/// - **可观测**:每个任务计时 + 日志,方便排查启动瓶颈
/// - **首帧优先**deferred 阶段等首帧渲染完再开始
class AppInitializer {
/// 关键任务(首帧前串行执行)
final List<InitTask> critical;
/// 延迟任务(首帧后串行执行)
final List<InitTask> deferred;
/// 日志回调
final void Function(String message, {String? tag})? onLog;
const AppInitializer({
this.critical = const [],
this.deferred = const [],
this.onLog,
});
/// 启动初始化
///
/// 1. 立即串行执行 critical 任务
/// 2. 注册 addPostFrameCallback首帧后串行执行 deferred 任务
///
/// 在 initState 中调用fire-and-forget不 await
void run() {
_runCritical();
}
/// 执行关键阶段
Future<void> _runCritical() async {
if (critical.isEmpty) {
_log('No critical tasks');
} else {
_log('── Critical phase: ${critical.length} task(s) ──');
final totalSw = Stopwatch()..start();
await _runTasksSequentially(critical);
totalSw.stop();
_log('── Critical phase done in ${totalSw.elapsedMilliseconds}ms ──');
}
// 关键阶段完成后,注册首帧回调执行延迟阶段
_scheduleDeferredPhase();
}
/// 注册 addPostFrameCallback
///
/// 需要在 critical 完成后再注册,确保顺序:
/// critical 完成 → 首帧渲染 → deferred 开始
void _scheduleDeferredPhase() {
if (deferred.isEmpty) return;
WidgetsBinding.instance.addPostFrameCallback((_) => _runDeferred());
}
/// 执行延迟阶段
Future<void> _runDeferred() async {
_log('── Deferred phase: ${deferred.length} task(s) ──');
final totalSw = Stopwatch()..start();
await _runTasksSequentially(deferred);
totalSw.stop();
_log('── Deferred phase done in ${totalSw.elapsedMilliseconds}ms ──');
}
/// 串行执行任务队列
///
/// 逐个 await不并发。每个任务独立 try-catch + 计时。
Future<void> _runTasksSequentially(List<InitTask> tasks) async {
for (var i = 0; i < tasks.length; i++) {
final task = tasks[i];
final sw = Stopwatch()..start();
try {
_log('[${i + 1}/${tasks.length}] ${task.name} ...');
await task.task();
sw.stop();
_log('[${i + 1}/${tasks.length}] ${task.name} done (${sw.elapsedMilliseconds}ms)');
} catch (e) {
sw.stop();
_log('[${i + 1}/${tasks.length}] ${task.name} FAILED (${sw.elapsedMilliseconds}ms): $e');
// 不 rethrow — 隔离失败,继续执行后续任务
}
}
}
void _log(String message) {
onLog?.call(message, tag: 'AppInit');
}
}
/// 初始化任务
class InitTask {
/// 任务名称(用于日志)
final String name;
/// 任务执行体
final Future<void> Function() task;
const InitTask({
required this.name,
required this.task,
});
}

View File

@@ -0,0 +1,105 @@
import 'dart:async';
import 'dart:math';
/// 网络恢复退避防抖器
///
/// 网络状态频繁切换WiFi 不稳定、隧道信号间断等)时,
/// 避免每次恢复都立即重连,用指数退避控制重连频率。
///
/// 增加 jitter 防止多设备同时重连的群体效应thundering herd
///
/// ## 退避策略
///
/// - 首次触发 → 等 baseDelay 后执行
/// - 短时间内再次触发 → 延迟翻倍(指数退避)
/// - 长时间静默后触发 → 重置为 baseDelay网络已稳定
///
/// ## 退避进程(默认参数)
///
/// ```
/// 触发 1 → 4s 后执行
/// 触发 2 → 8s 后执行
/// 触发 3 → 16s 后执行
/// 触发 4 → 32s 后执行
/// 触发 5 → 60s封顶
/// ...静默超过 2 分钟...
/// 触发 N → 4s重置
/// ```
///
/// ## 使用
///
/// ```dart
/// final debouncer = NetworkBackoffDebouncer();
///
/// // 网络恢复事件
/// debouncer.call(() {
/// socketManager.reconnect();
/// });
/// ```
class NetworkBackoffDebouncer {
/// 初始延迟
final Duration baseDelay;
/// 退避上限
final Duration maxDelay;
/// 静默多久后重置为初始延迟(网络已稳定,不再退避)
final Duration resetThreshold;
/// 退避倍数
final double factor;
Duration _currentDelay;
DateTime? _lastTriggerTime;
Timer? _timer;
final _random = Random();
NetworkBackoffDebouncer({
this.baseDelay = const Duration(seconds: 4),
this.maxDelay = const Duration(seconds: 60),
this.resetThreshold = const Duration(minutes: 2),
this.factor = 2.0,
}) : _currentDelay = baseDelay;
/// 触发退避执行
///
/// 取消上一个待执行的 action按当前退避延迟重新计时。
/// 短时间内多次触发只执行最后一次,延迟逐步递增。
void call(void Function() action) {
final now = DateTime.now();
if (_lastTriggerTime == null ||
now.difference(_lastTriggerTime!) > resetThreshold) {
// 首次触发 or 长时间静默 → 重置
_currentDelay = baseDelay;
} else {
// 短时间内再次触发 → 退避
final nextMs = (_currentDelay.inMilliseconds * factor).toInt();
_currentDelay = Duration(
milliseconds: nextMs < maxDelay.inMilliseconds
? nextMs
: maxDelay.inMilliseconds,
);
}
_lastTriggerTime = now;
// 加 jitter+0~25%),防止多设备同时重连
final jitterMs = _random.nextInt((_currentDelay.inMilliseconds * 0.25).toInt().clamp(1, 15000));
final delayWithJitter = _currentDelay + Duration(milliseconds: jitterMs);
_timer?.cancel();
_timer = Timer(delayWithJitter, action);
}
/// 取消待执行的 action
void cancel() {
_timer?.cancel();
_timer = null;
}
/// 释放资源
void dispose() {
cancel();
}
}

View File

@@ -0,0 +1,110 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
/// 网络状态监听
///
/// 基于 connectivity_plus 监听平台网络变化,
/// 提供 [isConnected] 查询和 [onStatusChanged] 事件流。
///
/// 非单例,由 Riverpod Provider 构造注入。
///
/// ## 数据流位置
///
/// ```
/// connectivity_plus平台网络事件
/// → ★ NetworkMonitor ★ ← 你在这里
/// → SocketManager.handleNetworkStatusChanged()
/// → SocketClient.connect / disconnect
/// ```
///
/// ## 使用
///
/// ```dart
/// final monitor = ref.read(networkMonitorProvider);
/// monitor.onStatusChanged.listen((isAvailable) {
/// // 网络状态变化
/// });
/// ```
class NetworkMonitor {
final Connectivity _connectivity = Connectivity();
StreamSubscription<List<ConnectivityResult>>? _subscription;
List<ConnectivityResult> _results = [];
final _statusController = StreamController<bool>.broadcast();
/// 日志输出回调
final void Function(String message, {String? tag})? onLog;
NetworkMonitor({this.onLog});
/// 当前是否有网络连接
///
/// 排除无连接和仅蓝牙连接的情况。
bool get isConnected {
if (_results.isEmpty) return false;
return !_results.contains(ConnectivityResult.none) &&
!_results.every((r) => r == ConnectivityResult.bluetooth);
}
/// 网络状态变化流
///
/// 只在连接状态真正改变时发送事件connected ↔ disconnected
/// 同类型切换WiFi → 4G不会触发。
Stream<bool> get onStatusChanged => _statusController.stream;
/// 初始化监听
///
/// App 启动时调用一次。获取当前状态并开始监听变化。
Future<void> initialize() async {
try {
_results = await _connectivity.checkConnectivity();
_log('Network status: $_connectionDescription');
} catch (e) {
_log('Failed to check connectivity: $e');
_results = [ConnectivityResult.none];
}
_subscription = _connectivity.onConnectivityChanged.listen(
_onChanged,
onError: (Object error) {
_log('Connectivity listener error: $error');
},
);
}
/// 释放资源
void dispose() {
_subscription?.cancel();
_statusController.close();
}
// ── 内部 ──────────────────────────────────────────────────────────────────
void _onChanged(List<ConnectivityResult> results) {
final wasConnected = isConnected;
_results = results;
final nowConnected = isConnected;
_log('Network changed: $_connectionDescription');
// 只在真正切换时通知
if (wasConnected != nowConnected) {
_statusController.add(nowConnected);
_log(nowConnected ? 'Network restored' : 'Network lost');
}
}
String get _connectionDescription {
if (_results.contains(ConnectivityResult.wifi)) return 'WiFi';
if (_results.contains(ConnectivityResult.mobile)) return 'Mobile';
if (_results.contains(ConnectivityResult.ethernet)) return 'Ethernet';
if (_results.contains(ConnectivityResult.none)) return 'None';
return 'Unknown';
}
void _log(String message) {
onLog?.call(message, tag: 'Network');
}
}

View File

@@ -0,0 +1,376 @@
import 'dart:async';
import 'package:networks_sdk/networks_sdk.dart';
import 'network_backoff_debouncer.dart';
/// 消息预处理回调
///
/// 参考 HTTP 层 onTokenRefresh 的回调注入模式。
/// App 层在 Provider 装配时注入解密/解析逻辑,
/// 不在 SDK 内部调用加解密 SDK。
typedef MessageTransformer = Map<String, dynamic> Function(
Map<String, dynamic> raw,
);
/// WebSocket 连接管理
///
/// 在 SocketClientSDK 底层能力)之上封装:
/// - 连接/断连生命周期(登录连接、登出断连)
/// - 前后台生命周期(后台断连省电、前台自动重连)
/// - 网络状态响应(断网断连、恢复网络立即重连)
/// - 操作前置检查(网络可用性 + 后台状态)
/// - 消息预处理管道(通过 [onMessageTransform] 回调注入解密等)
/// - 发送 API 透传
///
/// 不使用单例,通过 Riverpod Provider 注入。
///
/// ## 数据流位置
///
/// ```
/// SocketClient.messageStream原始消息
/// → onMessageTransform?解密回调App 层注入)
/// → ★ SocketManager.messageStream ★ ← 你在这里
/// → 业务模块消费
/// ```
///
/// ## 生命周期流程
///
/// ```
/// 登录成功 → connect(token) → 前置检查 → 建立连接
/// App 进后台 → onEnterBackground() → 断开连接(省电)
/// App 回前台 → onEnterForeground() → 检查网络 → 自动重连
/// 网络丢失 → handleNetworkLost() → 断开连接
/// 网络恢复 → handleNetworkRestored() → 退避重连(防抖动)
/// 登出 → disconnect() → 断开连接,清除 token
/// ```
///
/// ## 前置检查策略
///
/// 所有会发起网络操作的方法都先检查前置条件:
/// - connect → 检查网络可用性 + 是否在后台
/// - send / sendString → 检查连接状态 + 是否在后台
/// - onEnterForeground 重连 → 检查网络可用性
class SocketManager {
final NetworksMessagingApi _client;
final String _wsUrl;
/// 消息预处理回调
///
/// 登录后由 Provider 层注入,用于消息解密等。
/// 不注入时直接透传原始消息。
///
/// TODO: 接入加解密 SDK 后实现
final MessageTransformer? onMessageTransform;
/// 网络可用性查询App 层注入)
///
/// 与 HTTP 层 [ApiConfig.onCheckNetworkAvailable] 对称。
/// 连接和重连前调用,无网络时跳过操作并标记恢复时重试。
final Future<bool> Function()? onCheckNetworkAvailable;
/// 日志回调
final void Function(String message, {String? tag})? onLog;
// ── 内部状态 ──
/// 上次连接使用的 token用于前台/网络恢复时自动重连
String? _lastToken;
/// 后台断连标记:前台恢复时需要重连
bool _reconnectOnForeground = false;
/// 断网标记:网络恢复时需要重连
bool _reconnectOnNetworkRestore = false;
/// 当前是否在后台
bool _isInBackground = false;
/// 网络恢复退避防抖器
///
/// 网络抖动(快速 offline → online 切换)时,
/// 用指数退避避免反复锤服务器。
/// 通过 [NetworkBackoffDebouncer] 实现指数退避。
final NetworkBackoffDebouncer _networkDebouncer = NetworkBackoffDebouncer();
/// 前台恢复延迟重连定时器
///
/// 回前台后延迟 500ms 等待网络稳定再重连。
/// 期间如果再次进后台 / 主动断连 / dispose及时取消。
Timer? _foregroundReconnectTimer;
SocketManager({
required NetworksMessagingApi client,
required String wsUrl,
this.onMessageTransform,
this.onCheckNetworkAvailable,
this.onLog,
}) : _client = client,
_wsUrl = wsUrl;
// ── 连接 ──────────────────────────────────────────────────────────────────
/// 连接 WebSocket
///
/// 登录成功后调用token 从登录响应获取。
/// URL 由 Provider 层从 AppConfig 构建后注入,此处不关心来源。
///
/// 前置检查:
/// - 在后台 → 跳过,标记前台恢复时重连
/// - 无网络 → 跳过,标记网络恢复时重连
Future<bool> connect({required String token}) async {
_lastToken = token;
_reconnectOnForeground = false;
_reconnectOnNetworkRestore = false;
// 前置检查:在后台不连接(省电)
if (_isInBackground) {
_reconnectOnForeground = true;
_log('In background, defer connect to foreground');
return false;
}
// 前置检查:无网络不连接
if (!await _isNetworkAvailable()) {
_reconnectOnNetworkRestore = true;
_log('No network, defer connect to network restore');
return false;
}
_log('Connecting...');
return _client.connect(_wsUrl, token: token);
}
/// 断开连接(主动断连)
///
/// 登出时调用。清除 token取消所有待执行的重连不再自动重连。
Future<void> disconnect() async {
_lastToken = null;
_reconnectOnForeground = false;
_reconnectOnNetworkRestore = false;
_foregroundReconnectTimer?.cancel();
_foregroundReconnectTimer = null;
_networkDebouncer.cancel();
_log('Disconnecting (manual)');
await _client.disconnect();
}
/// 当前是否已连接
bool get isConnected => _client.isConnected;
/// 当前连接状态
SocketConnectionState get connectionState => _client.connectionState;
/// 当前是否在后台
bool get isInBackground => _isInBackground;
// ── 前后台生命周期 ────────────────────────────────────────────────────────
//
// 后台 → 断连(省电省流量)
// 前台 → 自动重连(如果之前有连接)
/// App 进后台 → 断开连接,标记前台恢复时重连
///
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
/// 后台保持连接会消耗电量和流量,断开后由 push 通知兜底。
void onEnterBackground() {
_isInBackground = true;
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
_foregroundReconnectTimer?.cancel();
_foregroundReconnectTimer = null;
// 同步 SocketClient 内部状态(与 onEnterForeground 对称)
_client.onEnterBackground();
if (_lastToken == null) return; // 未登录,无需处理
// 与 _handleNetworkLost 保持一致:
// 不仅 connectedconnecting / reconnecting 也要断开,
// 防止 SocketClient 在后台继续尝试连接浪费电量和流量。
if (_client.isConnected ||
_client.connectionState == SocketConnectionState.connecting ||
_client.connectionState == SocketConnectionState.reconnecting) {
_reconnectOnForeground = true;
_log('Entering background, disconnecting to save battery');
_client.disconnect();
}
}
/// App 回前台 → 自动重连(如果之前后台断连)
///
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.resumed] 时调用。
/// 重连前检查网络可用性,无网络时延迟到网络恢复事件再连。
void onEnterForeground() {
_isInBackground = false;
_client.onEnterForeground();
if (_reconnectOnForeground && _lastToken != null) {
_reconnectOnForeground = false;
_log('Returning to foreground, reconnecting...');
// 延迟 500ms 等待网络稳定,通过 Timer 跟踪以便进后台时取消
_foregroundReconnectTimer?.cancel();
_foregroundReconnectTimer = Timer(
const Duration(milliseconds: 500),
() async {
_foregroundReconnectTimer = null;
// 双重保险:回调执行时再次检查后台状态
if (_isInBackground) {
_reconnectOnForeground = true;
_log('Went back to background during delay, skip reconnect');
return;
}
if (!_client.isConnected && _lastToken != null) {
// 前置检查:网络可用性
if (!await _isNetworkAvailable()) {
_reconnectOnNetworkRestore = true;
_log('Network unavailable, defer reconnect to network restore');
return;
}
_client.connect(_wsUrl, token: _lastToken!);
}
},
);
}
}
// ── 网络状态变化 ──────────────────────────────────────────────────────────
//
// 网络丢失 → 断连(避免无效重试消耗资源)
// 网络恢复 → 退避重连(防网络抖动)
/// 网络状态变化处理
///
/// 由 App 层 NetworkMonitor.onStatusChanged 事件驱动。
void handleNetworkStatusChanged({required bool isAvailable}) {
if (isAvailable) {
_handleNetworkRestored();
} else {
_handleNetworkLost();
}
}
/// 网络丢失 → 断开连接,标记网络恢复时重连
///
/// 断网后继续重试没有意义,主动断连避免无效重连消耗资源。
void _handleNetworkLost() {
if (_lastToken == null) return; // 未登录,无需处理
if (_client.isConnected ||
_client.connectionState == SocketConnectionState.connecting ||
_client.connectionState == SocketConnectionState.reconnecting) {
_reconnectOnNetworkRestore = true;
_log('Network lost, disconnecting');
_client.disconnect();
}
}
/// 网络恢复 → 退避重连
///
/// 通过 [NetworkBackoffDebouncer] 控制重连频率,
/// 网络抖动(快速 offline/online 切换)时不会反复锤服务器。
///
/// 退避进程4s → 8s → 16s → 32s → 60s封顶
/// 网络稳定超过 2 分钟后重置。
void _handleNetworkRestored() {
if (_reconnectOnNetworkRestore && _lastToken != null) {
_reconnectOnNetworkRestore = false;
// 在后台不重连,等前台恢复时再连
if (_isInBackground) {
_reconnectOnForeground = true;
_log('Network restored but in background, defer to foreground');
return;
}
_log('Network restored, scheduling reconnect with backoff');
_networkDebouncer.call(() {
if (!_client.isConnected && _lastToken != null && !_isInBackground) {
_log('Backoff timer fired, reconnecting');
_client.connect(_wsUrl, token: _lastToken!);
}
});
}
}
// ── 消息流 ────────────────────────────────────────────────────────────────
/// 处理后的 JSON 消息流
///
/// 经过 [onMessageTransform] 预处理(解密等)后的消息。
/// 业务模块应监听此流,不直接监听 SocketClient.messageStream。
Stream<Map<String, dynamic>> get messageStream {
if (onMessageTransform != null) {
return _client.messageStream.map(onMessageTransform!);
}
return _client.messageStream;
}
/// 原始消息流(不经预处理,调试用)
Stream<String> get rawMessageStream => _client.rawMessageStream;
/// 连接状态变化流
Stream<SocketConnectionState> get connectionStateStream =>
_client.connectionStateStream;
/// 错误流
Stream<SocketError> get errorStream => _client.errorStream;
// ── 发送 ──────────────────────────────────────────────────────────────────
/// 发送 JSON 消息
///
/// 前置检查:未连接或在后台时不发送。
Future<bool> send(Map<String, dynamic> message) {
if (!_canSend()) return Future.value(false);
return _client.send(message);
}
/// 发送原始字符串
///
/// 前置检查:未连接或在后台时不发送。
Future<bool> sendString(String message) {
if (!_canSend()) return Future.value(false);
return _client.sendString(message);
}
// ── 释放 ──────────────────────────────────────────────────────────────────
/// 释放所有资源
Future<void> dispose() {
_foregroundReconnectTimer?.cancel();
_foregroundReconnectTimer = null;
_networkDebouncer.dispose();
return _client.dispose();
}
// ── 内部 ──────────────────────────────────────────────────────────────────
/// 发送前置检查
///
/// 两重保险:连接状态 + 后台状态。
/// 后台已断连所以 isConnected 通常就能拦住,
/// 但显式检查 _isInBackground 防止边界情况遗漏。
bool _canSend() {
if (!_client.isConnected) {
_log('Not connected, cannot send');
return false;
}
if (_isInBackground) {
_log('In background, skip send');
return false;
}
return true;
}
/// 查询网络可用性
///
/// 未注入回调时默认网络可用(不阻塞操作)。
Future<bool> _isNetworkAvailable() async {
if (onCheckNetworkAvailable == null) return true;
return onCheckNetworkAvailable!();
}
void _log(String message) {
onLog?.call(message, tag: 'SocketManager');
}
}