Files
customer-im-client-dev/apps/im_app/lib/app/di/network_provider.dart
2026-03-06 15:05:53 +08:00

405 lines
20 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:networks_sdk/networks_sdk.dart';
import '../../core/foundation/api_paths.dart';
import '../../core/foundation/config.dart';
import '../../core/foundation/constants.dart';
import '../../core/services/network_monitor.dart';
import '../../core/services/socket_manager.dart';
// ── 网络状态监听 ──────────────────────────────────────────────────────────────
/// 网络状态监听 Provider全局单例
///
/// 基于 connectivity_plus 监听平台网络变化,
/// 作为公共服务供多个模块使用:
/// - SocketManager网络变化时自动断连/重连 WebSocket
/// - HTTP 层:请求前检查网络可用性
/// - UI 层:显示网络状态提示
///
/// ## 使用
///
/// ```dart
/// // 查询当前状态
/// final isConnected = ref.read(networkMonitorProvider).isConnected;
///
/// // 监听状态变化
/// ref.listen(networkMonitorProvider, (prev, monitor) {
/// monitor.onStatusChanged.listen((isAvailable) {
/// // 处理网络变化
/// });
/// });
/// ```
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
final monitor = NetworkMonitor(
onLog: (message, {tag}) {
// ignore: avoid_print
print('[${tag ?? 'Network'}] $message');
},
);
ref.onDispose(() {
monitor.dispose();
});
return monitor;
});
// ── HTTP 基础设施 ─────────────────────────────────────────────────────────────
/// API 配置 Provider全局单例
///
/// 从 [AppConfig.apiBaseUrl]config.json → --dart-define-from-file读取 baseURL
/// 注入到 Network SDK 作为所有 HTTP 请求的基础 URL。
///
/// [onCheckNetworkAvailable] 由 [networkMonitorProvider](公共服务)注入,
/// 请求前先判断网络状态,无网络时直接抛 [ApiError.noNetworkConnection]。
final apiConfigProvider = Provider<ApiConfig>((ref) {
final networkMonitor = ref.read(networkMonitorProvider);
return ApiConfig(
baseURL: AppConfig.apiBaseUrl,
platformHeaders: {
'Platform': 'Android', // TODO: 运行时从平台 API 获取
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
},
tokenExpiredCodes: {30002, 30003, 30124},
forceLogoutCodes: {30125},
onForceLogout: () {
// TODO: 清除登录态,跳转登录页
},
onTokenRefresh: () async {
// TODO: App 层刷新 token 逻辑
return null;
},
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
maxRetries: AppConstants.maxRetries,
retryBaseDelay: AppConstants.retryBaseDelay,
onLog: (message, {tag}) {
// ignore: avoid_print
print('[${tag ?? 'Network'}] $message');
},
);
});
/// API 客户端 Provider全局单例
///
/// 含拦截器Auth / Retry / Logging、超时配置。
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
final config = ref.read(apiConfigProvider);
return NetworksSdkApi()..initialize(config);
});
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
/// SocketConfig Provider全局单例
///
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
/// SDK 内部不调用其他 SDK。
final socketConfigProvider = Provider<SocketConfig>((ref) {
final networkMonitor = ref.read(networkMonitorProvider);
return SocketConfig(
maxReconnectAttempts: AppConstants.maxRetries,
maxReconnectDelay: AppConstants.maxReconnectDelay,
onLog: (message, {tag}) {
// ignore: avoid_print
print('[${tag ?? 'Socket'}] $message');
},
onCheckNetworkAvailable: () async {
return networkMonitor.isConnected;
},
);
});
/// SocketClient Provider全局单例
///
/// 与 apiClientProvider 对称。
final socketClientProvider = Provider<NetworksMessagingApi>((ref)
{
final config = ref.read(socketConfigProvider);
return NetworksMessagingApi()..initialize(config);
});
/// SocketManager Provider
///
/// 封装连接生命周期、网络/前后台事件响应、操作前置检查、消息预处理。
/// 业务模块通过此 Provider 访问 WebSocket 能力。
///
/// ## 前置检查
///
/// connect / send 前先检查网络可用性 + 后台状态,
/// 无效操作直接跳过,避免无意义的网络请求。
/// 与 HTTP 层 [ApiClient.executeRequest] 的网络前置检查对称。
///
/// ## 事件驱动
///
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
/// 自动触发断连/重连。
///
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
/// 后续接入加解密 SDK 时,在此注入解密回调,
/// SDK 内部不调用其他 SDK。
final socketManagerProvider = Provider<SocketManager>((ref) {
final client = ref.read(socketClientProvider);
final networkMonitor = ref.read(networkMonitorProvider);
final manager = SocketManager(
client: client,
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
onLog: (message, {tag}) {
// ignore: avoid_print
print('[${tag ?? 'SocketManager'}] $message');
},
);
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
final subscription = networkMonitor.onStatusChanged.listen((isAvailable) {
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
});
ref.onDispose(() {
subscription.cancel();
unawaited(manager.dispose());
});
return manager;
});
// ── 辅助 ──────────────────────────────────────────────────────────────────────
/// HTTP baseURL → WebSocket URL 转换
///
/// https://api.example.com → wss://api.example.com/ws
/// http://api.example.com → ws://api.example.com/ws
String _buildWsUrl(String httpBaseUrl) {
String base = httpBaseUrl;
if (base.startsWith('https://')) {
base = base.replaceFirst('https://', 'wss://');
} else if (base.startsWith('http://')) {
base = base.replaceFirst('http://', 'ws://');
}
return '$base${ApiPaths.wsConnect}';
}
// ══════════════════════════════════════════════════════════════════════════════
// 本文件的职责
// ══════════════════════════════════════════════════════════════════════════════
//
// 提供所有网络基础设施 Provider网络监听 + HTTP + WebSocket。
// 业务模块的 DI 链路Repository → UseCase 按需)
// 内聚在 features/{模块}/di/{模块}_providers.dart 中。
//
// di/ 目录只放「需要手动装配的 Provider」构造注入、回调组合等
// ViewModel Provider 由 @riverpod 注解自动生成,不在 di/ 中。
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ DI 架构 │
// ├──────────────────────────────────────────────────────────────────────────┤
// │ app/di/ ← 手动装配SDK 基础设施 │
// │ ├── app_providers.dart → 主题 + 启动初始化 │
// │ └── network_provider.dart → 网络监听 + HTTP + WebSocket │
// │ │
// │ features/{模块}/di/ ← 手动装配:业务模块 DI 链路 │
// │ └── auth_providers.dart → Repository → UseCase按需
// │ chat_providers.dart 每个模块 DI 链路内聚在一个文件 │
// │ │
// │ features/{模块}/presentation/ ← @riverpod 自动生成ViewModel │
// │ └── login_view_model.dart → loginViewModelProvider代码生成
// └──────────────────────────────────────────────────────────────────────────┘
//
// Provider 链路:
//
// networkMonitorProvider公共服务HTTP + WS 共用)
// ├── apiConfigProvider → apiClientProvider ← HTTP 层
// └── socketConfigProvider → socketClientProvider ← WS 层
// → socketManagerProvider
//
// 网络事件驱动链路:
//
// connectivity_plus平台网络事件
// → NetworkMonitor.onStatusChangedtrue / false
// → SocketManager.handleNetworkStatusChanged()
// → 断网: disconnect()
// → 恢复: connect(token: lastToken)
//
// 前后台事件驱动链路:
//
// WidgetsBindingObserverApp 层 app.dart
// → SocketManager.onEnterBackground() → disconnect
// → SocketManager.onEnterForeground() → reconnect
//
// Repository 直接注入 ApiClient通过回调注入其他 SDK 能力:
//
// onTokenUpdate: (token) {
// apiConfig.updateToken(token); // 内存network_sdk
// secureStorage.saveToken(token); // 持久化crypto_sdk
// }
//
// 这样 network_sdk 和 crypto_sdk 互不依赖App 层是唯一知道两者的地方。
//
// ══════════════════════════════════════════════════════════════════════════════
// 新增接口的完整流程(以登录为例)
// ══════════════════════════════════════════════════════════════════════════════
//
// 「一个接口 = 一个 Request 文件」,严格按层调用,禁止跳层。
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ 文件 & 职责总览 │
// ├──────────────────────────────────────────────────────────────────────────┤
// │ login_request.dart Request + Response DTO一个端点一个文件
// │ auth_repository_impl.dart executeRequest → DTO → Entity + 回调写 Token│
// │ login_usecase.dart 格式校验 → 调 Repository按需非必须
// │ auth_providers.dart DI 装配Repository → UseCase 按需) │
// │ login_view_model.dart ref.read(authRepositoryProvider).login() │
// │ 或 ref.read(loginUseCaseProvider).execute() │
// │ login_page.dart ref.watch(loginViewModelProvider) │
// └──────────────────────────────────────────────────────────────────────────┘
//
// ─────────────────────────────────────────────────────────────────────────
// Step 1: 定义 Requestdata/remote/login_request.dart
// ─────────────────────────────────────────────────────────────────────────
//
// 一个文件包含两部分Response DTO + Request 类。
//
// @JsonSerializable()
// class LoginData { // Response DTO
// final String token;
// final String userId;
// factory LoginData.fromJson(Map<String, dynamic> json) => _$LoginDataFromJson(json);
// User toEntity() => User(id: userId, ...); // DTO → Domain Entity
// }
//
// @ApiRequest(path: ApiPaths.authLogin, method: HttpMethod.post,
// responseType: LoginData, requestType: ApiRequestType.login)
// @JsonSerializable()
// class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
// final String email;
// final String password;
// LoginRequest({required this.email, required this.password});
// @override Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
// }
//
// build_runner 生成 _$LoginRequestApi mixin → 自动提供 path / method / fromJson 注册。
//
// ─────────────────────────────────────────────────────────────────────────
// Step 2: Repositorydata/repositories/auth_repository_impl.dart
// ─────────────────────────────────────────────────────────────────────────
//
// class AuthRepositoryImpl implements AuthRepository {
// final ApiClient _client; // ← 直接注入 ApiClient
// final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
//
// Future<User> login({required String email, required String password}) async {
// final dto = await _client.executeRequest(
// LoginRequest(email: email, password: password),
// );
// _onTokenUpdate(dto!.token); // 回调写入 Token
// return dto.toEntity(); // DTO → Domain Entity
// }
// }
//
// ─────────────────────────────────────────────────────────────────────────
// Step 3: Provider 装配 + ViewModel
// ─────────────────────────────────────────────────────────────────────────
//
// // --- Provider 装配features/auth/di/auth_providers.dart ---
//
// // Repository直接注入 ApiClient + 回调组合多个 SDK 能力)
// final authRepositoryProvider = Provider((ref) {
// final apiConfig = ref.read(apiConfigProvider);
// return AuthRepositoryImpl(
// client: ref.read(apiClientProvider), // 直接注入
// onTokenUpdate: (token) {
// apiConfig.updateToken(token); // 内存network_sdk
// // secureStorage.saveToken(token); // 持久化crypto_sdk
// },
// );
// });
//
// // UseCase按需 — 登录有多步编排,所以需要)
// final loginUseCaseProvider = Provider((ref) {
// return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
// });
//
// // --- ViewModelfeatures/auth/presentation/login_view_model.dart ---
//
// // 常规写法ViewModel 直接调 Repository
// @riverpod
// class LoginViewModel extends _$LoginViewModel {
// Future<void> login(String email, String password) async {
// state = state.copyWith(isLoading: true);
// try {
// final user = await ref.read(authRepositoryProvider).login(
// email: email, password: password,
// );
// state = state.copyWith(user: user, isLoading: false);
// } on ApiError catch (e) {
// state = state.copyWith(error: e.displayMessage, isLoading: false);
// }
// }
// }
//
// // 进阶写法:有 UseCase 时(格式校验 + 多步编排)
// // final user = await ref.read(loginUseCaseProvider).execute(
// // email: email, password: password,
// // );
//
// ═════════════════════════════════════════════════════════════════════════
// 内部执行链路(点击登录按钮后发生了什么)
// ═════════════════════════════════════════════════════════════════════════
//
// View: vm.login(email, password)
// → ViewModel: ref.read(authRepositoryProvider).login(...) ← 常规路径
// → ViewModel: ref.read(loginUseCaseProvider).execute(...) ← 进阶路径(有 UseCase 时)
// → UseCase: 格式校验(邮箱 + 密码)
// → UseCase/ViewModel: authRepository.login(...)
// → Repository: _client.executeRequest(LoginRequest(...))
// → ApiClient.executeRequest(request)
// 1. 拼 URL: baseURL + "/auth/login"
// 2. request.parameters 触发 fromJson 自动注册
// 3. AuthInterceptor: 注入 token + platform headers
// 4. Dio.request(url, data: {email, password})
// 5. RetryInterceptor: token 过期 → 刷新 → 自动重试
// 6. LoggingInterceptor: 打印请求/响应日志
// → request.decodeResponse(response)
// 1. ApiResponseWrapper.fromJson: 拆 { code, message, data }
// 2. 检查 code != 0 → 抛 ApiError
// 3. fromJsonRegistry[LoginData] → LoginData.fromJson(data)
// → 返回 LoginDataDTO
// → _onTokenUpdate(token) 回调写入 TokenProvider 层组合:内存 + 持久化)
// → LoginData.toEntity() → UserDomain Entity
// → state.copyWith(user: user) 更新状态
// View: ref.watch → 自动 rebuild UI
//
// ═════════════════════════════════════════════════════════════════════════
// 各 HTTP 方法速查 — 新增接口时参照
// ═════════════════════════════════════════════════════════════════════════
//
// GET参数走 URL query string
// @ApiRequest(path: ..., method: HttpMethod.get, responseType: ProfileData)
// class GetProfileRequest extends ApiRequestable<ProfileData> with _$... { }
//
// POST参数走 JSON body
// 见上方 LoginRequest 示例。
//
// DELETE / PUT / PATCH与 POST 相同,只改 method
// @ApiRequest(path: ..., method: HttpMethod.delete, responseType: ...)
//
// POST 无响应数据(如 logout
// class LogoutRequest extends ApiRequestable<void> { ... }
// // → 返回 null
//
// Upload A: FormData 上传到自有后端
// @override Object? get uploadData => FormData.fromMap({ 'file': ... });
//
// Upload B: 二进制上传到 S3 presigned URL
// @override String get path => presignedURL; // 完整 URL不拼 baseURL
// @override Object? get uploadData => bytes; // Uint8List
// @override decodeResponse(response) { ... } // S3 不走标准信封
//