优化配置,修复 demo bug
1,network 框架完善 2,websocket 机制完善 3,设计文档整理到架构文档 4,脚本,配置完善
This commit is contained in:
@@ -21,6 +21,7 @@ plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
@@ -38,5 +38,10 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
# 对没有在 podspec 里声明 swift_version 的 pod 设置兜底版本。
|
||||
# 已有 swift_version 的 pod(含第三方如 Agora)CocoaPods 优先使用其 podspec 值,不受影响。
|
||||
config.build_settings['SWIFT_VERSION'] ||= '6.2'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -164,7 +164,6 @@
|
||||
1C416905D0EA345032C4E612 /* Pods-RunnerTests.release.xcconfig */,
|
||||
9538107A41BCB5B5D84FBAF3 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -489,7 +488,7 @@
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
@@ -508,7 +507,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -524,7 +523,7 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
@@ -540,7 +539,7 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
@@ -678,7 +677,7 @@
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
@@ -705,7 +704,7 @@
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
|
||||
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
@@ -16,9 +16,11 @@ import UIKit
|
||||
sceneConfig.delegateClass = SceneDelegate.self
|
||||
return sceneConfig
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FlutterImplicitEngineDelegate
|
||||
|
||||
// FlutterImplicitEngineDelegate 来自 Flutter ObjC 框架,尚未标注 @MainActor,
|
||||
// 用 @preconcurrency 抑制 Swift 6 ConformanceIsolation 错误。
|
||||
extension AppDelegate: @preconcurrency FlutterImplicitEngineDelegate {
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ 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/foundation/errors.dart';
|
||||
import '../../core/foundation/utils.dart';
|
||||
import '../../core/services/network_monitor.dart';
|
||||
import '../../core/services/socket_manager.dart';
|
||||
|
||||
@@ -47,6 +49,21 @@ final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||||
return monitor;
|
||||
});
|
||||
|
||||
// ── Token 更新事件流 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Token 更新事件流
|
||||
///
|
||||
/// apiConfigProvider.onTokenUpdated → 推送新 token 到此流
|
||||
/// socketManagerProvider → 监听此流 → 同步 token 到 WebSocket
|
||||
/// onBeforeReconnect 中刷新 token 后调用 apiConfig.updateToken → tokenStream.add,
|
||||
/// 需要同步传播到 socketManager.updateToken → socketClient._currentToken,
|
||||
/// 确保随后的 _doConnect() 使用新 token。异步模式下 _doConnect 会在 stream
|
||||
final _tokenUpdateStreamProvider = Provider<StreamController<String>>((ref) {
|
||||
final controller = StreamController<String>.broadcast(sync: true);
|
||||
ref.onDispose(controller.close);
|
||||
return controller;
|
||||
});
|
||||
|
||||
// ── HTTP 基础设施 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// API 配置 Provider(全局单例)
|
||||
@@ -58,15 +75,18 @@ final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
|
||||
/// 请求前先判断网络状态,无网络时直接抛 [ApiError.noNetworkConnection]。
|
||||
final apiConfigProvider = Provider<ApiConfig>((ref) {
|
||||
final networkMonitor = ref.read(networkMonitorProvider);
|
||||
final tokenStream = ref.read(_tokenUpdateStreamProvider);
|
||||
|
||||
return ApiConfig(
|
||||
baseURL: AppConfig.apiBaseUrl,
|
||||
platformHeaders: {
|
||||
'Platform': 'Android', // TODO: 运行时从平台 API 获取
|
||||
'Platform': 'Android', // TODO: 运行时从 platform API 获取
|
||||
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
|
||||
'Channel': '', // TODO: 从 AppConfig 读取渠道标识
|
||||
'lang': 'zh-CN', // TODO: 从 l10n_sdk 或系统 locale 动态获取
|
||||
},
|
||||
tokenExpiredCodes: {30002, 30003, 30124},
|
||||
forceLogoutCodes: {30125},
|
||||
tokenExpiredCodes: ApiErrorCodes.tokenExpiredCodes,
|
||||
forceLogoutCodes: ApiErrorCodes.forceLogoutCodes,
|
||||
onForceLogout: () {
|
||||
// TODO: 清除登录态,跳转登录页
|
||||
},
|
||||
@@ -74,7 +94,17 @@ final apiConfigProvider = Provider<ApiConfig>((ref) {
|
||||
// TODO: App 层刷新 token 逻辑
|
||||
return null;
|
||||
},
|
||||
onTokenUpdated: (newToken) {
|
||||
// 通过事件流同步到 WebSocket,避免直接引用 socketManagerProvider 造成循环依赖
|
||||
tokenStream.add(newToken);
|
||||
},
|
||||
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
||||
onEncryptRequest: null, // TODO: 接入 cipher_guard_sdk 后注入请求加密回调
|
||||
onDecryptResponse: null, // TODO: 接入 cipher_guard_sdk 后注入响应解密回调
|
||||
onBusinessError: null, // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等)
|
||||
onTransformResponse:
|
||||
null, // TODO: 如后端信封结构非标准,在此归一化为 { code, data, message }
|
||||
onGetTokenExpiry: parseJwtExpiry,
|
||||
maxRetries: AppConstants.maxRetries,
|
||||
retryBaseDelay: AppConstants.retryBaseDelay,
|
||||
onLog: (message, {tag}) {
|
||||
@@ -94,16 +124,47 @@ final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
|
||||
|
||||
// ── WebSocket 基础设施 ────────────────────────────────────────────────────────
|
||||
|
||||
/// SocketConfig Provider(全局单例)
|
||||
/// SocketConfig Provider(内部使用,不对外暴露)
|
||||
///
|
||||
/// 与 apiConfigProvider 对称,通过回调注入 App 层能力,
|
||||
/// SDK 内部不调用其他 SDK。
|
||||
final socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||
final _socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||
final networkMonitor = ref.read(networkMonitorProvider);
|
||||
|
||||
return SocketConfig(
|
||||
maxReconnectAttempts: AppConstants.maxRetries,
|
||||
maxReconnectDelay: AppConstants.maxReconnectDelay,
|
||||
unlimitedReconnect: true, // IM 场景始终保持连接
|
||||
onBuildConnectUrl:
|
||||
null, // TODO: 接入 cipher_guard_sdk 后注入 WS URL 加密(路径/token/cipher 参数)
|
||||
onEncryptMessage: null, // TODO: 接入 cipher_guard_sdk 后注入消息加密回调
|
||||
onDecryptMessage: null, // TODO: 接入 cipher_guard_sdk 后注入消息解密回调
|
||||
onBeforeReconnect: () async {
|
||||
// SocketClient 内部重连(心跳超时、stream onDone)前调用。
|
||||
// 与 SocketManager.onBeforeReconnect 职责相同:检查 token 并按需刷新。
|
||||
// 刷新后通过 sync stream 同步传播到 SocketClient._currentToken,
|
||||
// 确保随后的 _doConnect() 使用新 token。
|
||||
final apiConfig = ref.read(apiConfigProvider);
|
||||
final currentToken = apiConfig.token;
|
||||
if (currentToken == null || apiConfig.onGetTokenExpiry == null) return;
|
||||
|
||||
final expiry = apiConfig.onGetTokenExpiry!(currentToken);
|
||||
if (expiry == null) return;
|
||||
|
||||
final remaining = expiry.difference(DateTime.now());
|
||||
if (remaining > apiConfig.proactiveRefreshThreshold) return;
|
||||
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'[Socket] Token expiring in ${remaining.inMinutes}min, refreshing before reconnect',
|
||||
);
|
||||
final newToken = await apiConfig.onTokenRefresh?.call();
|
||||
if (newToken != null && newToken.isNotEmpty) {
|
||||
// updateToken → onTokenUpdated → sync stream → manager.updateToken
|
||||
// → _client.updateToken → socketClient._currentToken 同步更新
|
||||
apiConfig.updateToken(newToken);
|
||||
}
|
||||
},
|
||||
onLog: (message, {tag}) {
|
||||
// ignore: avoid_print
|
||||
print('[${tag ?? 'Socket'}] $message');
|
||||
@@ -114,12 +175,11 @@ final socketConfigProvider = Provider<SocketConfig>((ref) {
|
||||
);
|
||||
});
|
||||
|
||||
/// SocketClient Provider(全局单例)
|
||||
/// SocketClient Provider(内部使用,不对外暴露)
|
||||
///
|
||||
/// 与 apiClientProvider 对称。
|
||||
final socketClientProvider = Provider<NetworksMessagingApi>((ref)
|
||||
{
|
||||
final config = ref.read(socketConfigProvider);
|
||||
/// 与 networkSdkApiProvider 对称。
|
||||
final _socketClientProvider = Provider<NetworksMessagingApi>((ref) {
|
||||
final config = ref.read(_socketConfigProvider);
|
||||
return NetworksMessagingApi()..initialize(config);
|
||||
});
|
||||
|
||||
@@ -139,17 +199,43 @@ final socketClientProvider = Provider<NetworksMessagingApi>((ref)
|
||||
/// 网络状态变化由 [networkMonitorProvider](公共服务)驱动,
|
||||
/// 自动触发断连/重连。
|
||||
///
|
||||
/// Token 更新由 [_tokenUpdateStreamProvider] 事件流驱动,
|
||||
/// HTTP 层刷新 token 后自动同步到 WebSocket。
|
||||
///
|
||||
/// onMessageTransform 参考 HTTP 层 onTokenRefresh 的回调模式:
|
||||
/// 后续接入加解密 SDK 时,在此注入解密回调,
|
||||
/// SDK 内部不调用其他 SDK。
|
||||
final socketManagerProvider = Provider<SocketManager>((ref) {
|
||||
final client = ref.read(socketClientProvider);
|
||||
final client = ref.read(_socketClientProvider);
|
||||
final networkMonitor = ref.read(networkMonitorProvider);
|
||||
final apiConfig = ref.read(apiConfigProvider);
|
||||
final tokenStream = ref.read(_tokenUpdateStreamProvider);
|
||||
|
||||
final manager = SocketManager(
|
||||
client: client,
|
||||
wsUrl: _buildWsUrl(AppConfig.apiBaseUrl),
|
||||
onMessageTransform: null, // TODO: 接入加解密 SDK 后注入解密回调
|
||||
onBeforeReconnect: () async {
|
||||
// 重连前检查 token 是否即将过期,是则主动刷新
|
||||
final currentToken = apiConfig.token;
|
||||
if (currentToken == null || apiConfig.onGetTokenExpiry == null) return;
|
||||
|
||||
final expiry = apiConfig.onGetTokenExpiry!(currentToken);
|
||||
if (expiry == null) return;
|
||||
|
||||
final remaining = expiry.difference(DateTime.now());
|
||||
if (remaining > apiConfig.proactiveRefreshThreshold) return;
|
||||
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'[SocketManager] Token expiring in ${remaining.inMinutes}min, refreshing before reconnect',
|
||||
);
|
||||
final newToken = await apiConfig.onTokenRefresh?.call();
|
||||
if (newToken != null && newToken.isNotEmpty) {
|
||||
// updateToken 触发 onTokenUpdated → tokenStream → socketManager.updateToken
|
||||
apiConfig.updateToken(newToken);
|
||||
}
|
||||
},
|
||||
onCheckNetworkAvailable: () async => networkMonitor.isConnected,
|
||||
onLog: (message, {tag}) {
|
||||
// ignore: avoid_print
|
||||
@@ -157,13 +243,19 @@ final socketManagerProvider = Provider<SocketManager>((ref) {
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 token 更新事件 → 同步到 WebSocket
|
||||
final tokenSub = tokenStream.stream.listen((newToken) {
|
||||
manager.updateToken(newToken);
|
||||
});
|
||||
|
||||
// 监听网络状态变化 → 驱动 SocketManager 断连/重连
|
||||
final subscription = networkMonitor.onStatusChanged.listen((isAvailable) {
|
||||
final networkSub = networkMonitor.onStatusChanged.listen((isAvailable) {
|
||||
manager.handleNetworkStatusChanged(isAvailable: isAvailable);
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
tokenSub.cancel();
|
||||
networkSub.cancel();
|
||||
unawaited(manager.dispose());
|
||||
});
|
||||
|
||||
@@ -215,23 +307,55 @@ String _buildWsUrl(String httpBaseUrl) {
|
||||
// Provider 链路:
|
||||
//
|
||||
// networkMonitorProvider(公共服务,HTTP + WS 共用)
|
||||
// ├── apiConfigProvider → apiClientProvider ← HTTP 层
|
||||
// └── socketConfigProvider → socketClientProvider ← WS 层
|
||||
// ├── apiConfigProvider → networkSdkApiProvider ← HTTP 层
|
||||
// └── _socketConfigProvider → _socketClientProvider ← WS 层(内部)
|
||||
// → socketManagerProvider
|
||||
//
|
||||
// _tokenUpdateStreamProvider(打破循环引用的中间层)
|
||||
// ← apiConfigProvider.onTokenUpdated 推送
|
||||
// → socketManagerProvider 监听 → socketManager.updateToken()
|
||||
//
|
||||
// 网络事件驱动链路:
|
||||
//
|
||||
// connectivity_plus(平台网络事件)
|
||||
// → NetworkMonitor.onStatusChanged(true / false)
|
||||
// → SocketManager.handleNetworkStatusChanged()
|
||||
// → 断网: disconnect()
|
||||
// → 恢复: connect(token: lastToken)
|
||||
// → 恢复: onBeforeReconnect → connect(token: lastToken)
|
||||
//
|
||||
// 前后台事件驱动链路:
|
||||
//
|
||||
// WidgetsBindingObserver(App 层 app.dart)
|
||||
// → SocketManager.onEnterBackground() → disconnect
|
||||
// → SocketManager.onEnterForeground() → reconnect
|
||||
// → SocketManager.onEnterBackground()
|
||||
// disconnectInBackground=true → disconnect(默认,移动端省电)
|
||||
// disconnectInBackground=false → 完全保活,不断连不暂停心跳(桌面端)
|
||||
// → SocketManager.onEnterForeground() → onBeforeReconnect → reconnect
|
||||
//
|
||||
// Token 刷新 → WebSocket 同步链路:
|
||||
//
|
||||
// RetryInterceptor 检测 token 过期
|
||||
// → TokenRefreshManager.refreshIfNeeded()
|
||||
// → apiConfig.updateToken(newToken)
|
||||
// → onTokenUpdated(newToken)
|
||||
// → _tokenUpdateStream.add(newToken)
|
||||
// → socketManager.updateToken(newToken) // 不断连,下次重连自动用新 token
|
||||
//
|
||||
// 主动 token 刷新(重连前,两个层级):
|
||||
//
|
||||
// SocketManager 层(前台恢复 / 网络恢复触发):
|
||||
// SocketManager.onBeforeReconnect()
|
||||
// → 解析 JWT exp → 距过期 < 阈值
|
||||
// → apiConfig.onTokenRefresh() → 刷新
|
||||
// → apiConfig.updateToken(newToken)
|
||||
// → sync stream → manager.updateToken → _lastToken 更新
|
||||
// → _client.connect(token: _lastToken) 使用新 token
|
||||
//
|
||||
// SocketClient 层(心跳超时 / stream onDone 触发):
|
||||
// SocketConfig.onBeforeReconnect()
|
||||
// → 同上逻辑:检查 JWT exp → 刷新 → apiConfig.updateToken
|
||||
// → sync stream → manager.updateToken → _client.updateToken
|
||||
// → socketClient._currentToken 同步更新
|
||||
// → _doConnect() 使用新 token
|
||||
//
|
||||
// Repository 直接注入 ApiClient,通过回调注入其他 SDK 能力:
|
||||
//
|
||||
@@ -313,7 +437,7 @@ String _buildWsUrl(String httpBaseUrl) {
|
||||
// final authRepositoryProvider = Provider((ref) {
|
||||
// final apiConfig = ref.read(apiConfigProvider);
|
||||
// return AuthRepositoryImpl(
|
||||
// client: ref.read(apiClientProvider), // 直接注入
|
||||
// client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||
// onTokenUpdate: (token) {
|
||||
// apiConfig.updateToken(token); // 内存(network_sdk)
|
||||
// // secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/// API 错误码常量
|
||||
///
|
||||
/// 集中管理后端业务错误码,避免散落在各处硬编码。
|
||||
/// 按业务域分组,命名风格对齐后端定义。
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```dart
|
||||
/// ApiConfig(
|
||||
/// tokenExpiredCodes: ApiErrorCodes.tokenExpiredCodes,
|
||||
/// forceLogoutCodes: ApiErrorCodes.forceLogoutCodes,
|
||||
/// )
|
||||
/// ```
|
||||
class ApiErrorCodes {
|
||||
ApiErrorCodes._();
|
||||
|
||||
// ── 认证(30001-30009)──
|
||||
|
||||
/// Token 无效
|
||||
static const int tokenInvalid = 30002;
|
||||
|
||||
/// JWT 无效
|
||||
static const int jwtInvalid = 30003;
|
||||
|
||||
/// 签名方法错误
|
||||
static const int signingMethodError = 30008;
|
||||
|
||||
/// 密钥解析失败
|
||||
static const int parsingKeyError = 30009;
|
||||
|
||||
/// Session 无效
|
||||
static const int sessionInvalid = 30124;
|
||||
|
||||
/// Refresh Token 失效
|
||||
static const int refreshTokenFailed = 30125;
|
||||
|
||||
/// 账号在其他设备登录
|
||||
static const int loggedInAnotherDevice = 30006;
|
||||
|
||||
// ── 错误码集合 ──
|
||||
|
||||
/// Token 过期错误码集合 — 触发自动刷新 Token
|
||||
static const Set<int> tokenExpiredCodes = {
|
||||
tokenInvalid,
|
||||
jwtInvalid,
|
||||
sessionInvalid,
|
||||
};
|
||||
|
||||
/// 强制登出错误码集合 — 触发退出登录流程
|
||||
static const Set<int> forceLogoutCodes = {refreshTokenFailed};
|
||||
|
||||
/// 踢下线错误码集合 — 触发踢下线 UI 提示
|
||||
static const Set<int> kickOffCodes = {
|
||||
loggedInAnotherDevice,
|
||||
signingMethodError,
|
||||
parsingKeyError,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
|
||||
/// JWT token 过期时间解析
|
||||
///
|
||||
/// 使用 dart_jsonwebtoken 解码 JWT payload,提取 `exp` claim 返回过期时间。
|
||||
/// 返回 null 表示无法解析(非 JWT 格式或缺少 exp 字段)。
|
||||
///
|
||||
/// 只读取 payload,不验证签名(验证是服务端的事)。
|
||||
///
|
||||
/// 用于 [ApiConfig.onGetTokenExpiry] 回调,启用 token 主动刷新:
|
||||
/// 距过期不足阈值时提前刷新,避免带过期 token 发请求或重连。
|
||||
///
|
||||
/// ```dart
|
||||
/// final expiry = parseJwtExpiry('eyJhbGci...');
|
||||
/// if (expiry != null) {
|
||||
/// final remaining = expiry.difference(DateTime.now());
|
||||
/// print('Token expires in ${remaining.inMinutes} min');
|
||||
/// }
|
||||
/// ```
|
||||
DateTime? parseJwtExpiry(String token) {
|
||||
try {
|
||||
final jwt = JWT.decode(token);
|
||||
final payload = jwt.payload;
|
||||
if (payload is! Map<String, dynamic>) return null;
|
||||
|
||||
final exp = payload['exp'];
|
||||
if (exp is! int) return null;
|
||||
return DateTime.fromMillisecondsSinceEpoch(exp * 1000);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import 'network_backoff_debouncer.dart';
|
||||
@@ -10,9 +9,8 @@ import 'network_backoff_debouncer.dart';
|
||||
/// 参考 HTTP 层 onTokenRefresh 的回调注入模式。
|
||||
/// App 层在 Provider 装配时注入解密/解析逻辑,
|
||||
/// 不在 SDK 内部调用加解密 SDK。
|
||||
typedef MessageTransformer = Map<String, dynamic> Function(
|
||||
Map<String, dynamic> raw,
|
||||
);
|
||||
typedef MessageTransformer =
|
||||
Map<String, dynamic> Function(Map<String, dynamic> raw);
|
||||
|
||||
/// WebSocket 连接管理
|
||||
///
|
||||
@@ -39,19 +37,26 @@ typedef MessageTransformer = Map<String, dynamic> Function(
|
||||
///
|
||||
/// ```
|
||||
/// 登录成功 → connect(token) → 前置检查 → 建立连接
|
||||
/// App 进后台 → onEnterBackground() → 断开连接(省电)
|
||||
/// App 回前台 → onEnterForeground() → 检查网络 → 自动重连
|
||||
///
|
||||
/// ── disconnectInBackground = true(默认,移动端)──
|
||||
/// App 进后台 → onEnterBackground() → 暂停心跳 + 断开连接(省电)
|
||||
/// App 回前台 → onEnterForeground() → 恢复心跳 → onBeforeReconnect → 重连
|
||||
///
|
||||
/// ── disconnectInBackground = false(桌面端)──
|
||||
/// App 进后台 → onEnterBackground() → 不操作,完全保活
|
||||
/// App 回前台 → onEnterForeground() → 不操作(连接始终在线)
|
||||
///
|
||||
/// 网络丢失 → handleNetworkLost() → 断开连接
|
||||
/// 网络恢复 → handleNetworkRestored() → 退避重连(防抖动)
|
||||
/// 网络恢复 → handleNetworkRestored() → 退避 → onBeforeReconnect → 重连
|
||||
/// 登出 → disconnect() → 断开连接,清除 token
|
||||
/// ```
|
||||
///
|
||||
/// ## 前置检查策略
|
||||
///
|
||||
/// 所有会发起网络操作的方法都先检查前置条件:
|
||||
/// - connect → 检查网络可用性 + 是否在后台
|
||||
/// - send / sendString → 检查连接状态 + 是否在后台
|
||||
/// - onEnterForeground 重连 → 检查网络可用性
|
||||
/// - connect → 检查网络可用性 + 是否在后台(仅 disconnectInBackground=true 时拦截)
|
||||
/// - send / sendString → 检查连接状态 + 是否在后台(仅 disconnectInBackground=true 时拦截)
|
||||
/// - onEnterForeground / 网络恢复重连 → 检查网络可用性 + onBeforeReconnect
|
||||
class SocketManager {
|
||||
final NetworksMessagingApi _client;
|
||||
final String _wsUrl;
|
||||
@@ -70,6 +75,22 @@ class SocketManager {
|
||||
/// 连接和重连前调用,无网络时跳过操作并标记恢复时重试。
|
||||
final Future<bool> Function()? onCheckNetworkAvailable;
|
||||
|
||||
/// 重连前回调
|
||||
///
|
||||
/// 在 WebSocket 重连前调用(前台恢复、网络恢复),App 层用于:
|
||||
/// - 检查并刷新即将过期的 token
|
||||
/// - 更新连接参数
|
||||
///
|
||||
/// 回调完成后才发起实际重连。
|
||||
final Future<void> Function()? onBeforeReconnect;
|
||||
|
||||
/// 进后台时是否断开连接
|
||||
///
|
||||
/// true(默认)— 后台断连省电,由 push 通知兜底,前台恢复时自动重连。
|
||||
/// false — 后台保持连接(适用于桌面端或需要后台实时推送的场景)。
|
||||
/// 设为 false 时,后台仅暂停心跳,不主动断连。
|
||||
final bool disconnectInBackground;
|
||||
|
||||
/// 日志回调
|
||||
final void Function(String message, {String? tag})? onLog;
|
||||
|
||||
@@ -104,10 +125,12 @@ class SocketManager {
|
||||
required NetworksMessagingApi client,
|
||||
required String wsUrl,
|
||||
this.onMessageTransform,
|
||||
this.onBeforeReconnect,
|
||||
this.disconnectInBackground = true,
|
||||
this.onCheckNetworkAvailable,
|
||||
this.onLog,
|
||||
}) : _client = client,
|
||||
_wsUrl = wsUrl;
|
||||
}) : _client = client,
|
||||
_wsUrl = wsUrl;
|
||||
|
||||
// ── 连接 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -124,8 +147,8 @@ class SocketManager {
|
||||
_reconnectOnForeground = false;
|
||||
_reconnectOnNetworkRestore = false;
|
||||
|
||||
// 前置检查:在后台不连接(省电)
|
||||
if (_isInBackground) {
|
||||
// 前置检查:移动端模式下在后台不连接(省电)
|
||||
if (_isInBackground && disconnectInBackground) {
|
||||
_reconnectOnForeground = true;
|
||||
_log('In background, defer connect to foreground');
|
||||
return false;
|
||||
@@ -165,26 +188,47 @@ class SocketManager {
|
||||
/// 当前是否在后台
|
||||
bool get isInBackground => _isInBackground;
|
||||
|
||||
/// Token 热更新
|
||||
///
|
||||
/// 透传给 SocketClient,仅更新内部 token,不断开连接。
|
||||
/// 适用于 HTTP 层 token 刷新后同步到 WebSocket 的场景。
|
||||
void updateToken(String token) {
|
||||
_lastToken = token;
|
||||
_client.updateToken(token);
|
||||
_log('Token updated via SocketManager');
|
||||
}
|
||||
|
||||
// ── 前后台生命周期 ────────────────────────────────────────────────────────
|
||||
//
|
||||
// 后台 → 断连(省电省流量)
|
||||
// 后台 → 断连(省电省流量)或保持连接(桌面端)
|
||||
// 前台 → 自动重连(如果之前有连接)
|
||||
|
||||
/// App 进后台 → 断开连接,标记前台恢复时重连
|
||||
/// App 进后台
|
||||
///
|
||||
/// 由 App 层 WidgetsBindingObserver 在 [AppLifecycleState.paused] 时调用。
|
||||
/// 后台保持连接会消耗电量和流量,断开后由 push 通知兜底。
|
||||
///
|
||||
/// [disconnectInBackground] 为 true 时(默认,移动端):
|
||||
/// 断开连接 + 暂停心跳,由 push 通知兜底,前台恢复时自动重连。
|
||||
///
|
||||
/// [disconnectInBackground] 为 false 时(桌面端):
|
||||
/// 不断连、不暂停心跳,WebSocket 完全保活。
|
||||
void onEnterBackground() {
|
||||
_isInBackground = true;
|
||||
// 取消待执行的前台重连(防止快速 前台→后台 切换导致后台建连)
|
||||
_foregroundReconnectTimer?.cancel();
|
||||
_foregroundReconnectTimer = null;
|
||||
// 同步 SocketClient 内部状态(与 onEnterForeground 对称)
|
||||
|
||||
if (!disconnectInBackground) {
|
||||
// 桌面端模式:不断连、不暂停心跳,完全保活
|
||||
_log('Entering background, keeping connection alive');
|
||||
return;
|
||||
}
|
||||
|
||||
// 移动端模式:通知 SocketClient 进后台(暂停心跳)
|
||||
_client.onEnterBackground();
|
||||
|
||||
if (_lastToken == null) return; // 未登录,无需处理
|
||||
|
||||
// 与 _handleNetworkLost 保持一致:
|
||||
// 不仅 connected,connecting / reconnecting 也要断开,
|
||||
// 防止 SocketClient 在后台继续尝试连接浪费电量和流量。
|
||||
if (_client.isConnected ||
|
||||
@@ -202,7 +246,11 @@ class SocketManager {
|
||||
/// 重连前检查网络可用性,无网络时延迟到网络恢复事件再连。
|
||||
void onEnterForeground() {
|
||||
_isInBackground = false;
|
||||
_client.onEnterForeground();
|
||||
|
||||
// 只在移动端模式(后台曾断连/暂停心跳)时通知 SocketClient 恢复
|
||||
if (disconnectInBackground) {
|
||||
_client.onEnterForeground();
|
||||
}
|
||||
|
||||
if (_reconnectOnForeground && _lastToken != null) {
|
||||
_reconnectOnForeground = false;
|
||||
@@ -226,7 +274,12 @@ class SocketManager {
|
||||
_log('Network unavailable, defer reconnect to network restore');
|
||||
return;
|
||||
}
|
||||
_client.connect(_wsUrl, token: _lastToken!);
|
||||
// 重连前钩子:刷新即将过期的 token 等
|
||||
await onBeforeReconnect?.call();
|
||||
// token 可能被 onBeforeReconnect 更新(通过 updateToken 链路同步)
|
||||
if (_lastToken != null && !_client.isConnected) {
|
||||
_client.connect(_wsUrl, token: _lastToken!);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -275,18 +328,22 @@ class SocketManager {
|
||||
if (_reconnectOnNetworkRestore && _lastToken != null) {
|
||||
_reconnectOnNetworkRestore = false;
|
||||
|
||||
// 在后台不重连,等前台恢复时再连
|
||||
if (_isInBackground) {
|
||||
// 移动端模式:在后台不重连,等前台恢复时再连
|
||||
if (_isInBackground && disconnectInBackground) {
|
||||
_reconnectOnForeground = true;
|
||||
_log('Network restored but in background, defer to foreground');
|
||||
return;
|
||||
}
|
||||
|
||||
_log('Network restored, scheduling reconnect with backoff');
|
||||
_networkDebouncer.call(() {
|
||||
_networkDebouncer.call(() async {
|
||||
if (!_client.isConnected && _lastToken != null && !_isInBackground) {
|
||||
_log('Backoff timer fired, reconnecting');
|
||||
_client.connect(_wsUrl, token: _lastToken!);
|
||||
// 重连前钩子:刷新即将过期的 token 等
|
||||
await onBeforeReconnect?.call();
|
||||
if (!_client.isConnected && _lastToken != null && !_isInBackground) {
|
||||
_log('Backoff timer fired, reconnecting');
|
||||
_client.connect(_wsUrl, token: _lastToken!);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -308,6 +365,9 @@ class SocketManager {
|
||||
/// 原始消息流(不经预处理,调试用)
|
||||
Stream<String> get rawMessageStream => _client.rawMessageStream;
|
||||
|
||||
/// 二进制消息流
|
||||
Stream<dynamic> get binaryMessageStream => _client.binaryMessageStream;
|
||||
|
||||
/// 连接状态变化流
|
||||
Stream<SocketConnectionState> get connectionStateStream =>
|
||||
_client.connectionStateStream;
|
||||
@@ -333,6 +393,14 @@ class SocketManager {
|
||||
return _client.sendString(message);
|
||||
}
|
||||
|
||||
/// 发送二进制数据
|
||||
///
|
||||
/// 前置检查:未连接或在后台时不发送。
|
||||
Future<bool> sendBytes(List<int> bytes) {
|
||||
if (!_canSend()) return Future.value(false);
|
||||
return _client.sendBytes(bytes);
|
||||
}
|
||||
|
||||
// ── 释放 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 释放所有资源
|
||||
@@ -355,7 +423,7 @@ class SocketManager {
|
||||
_log('Not connected, cannot send');
|
||||
return false;
|
||||
}
|
||||
if (_isInBackground) {
|
||||
if (_isInBackground && disconnectInBackground) {
|
||||
_log('In background, skip send');
|
||||
return false;
|
||||
}
|
||||
|
||||
30
apps/im_app/lib/core/ui/base/assets.dart
Normal file
30
apps/im_app/lib/core/ui/base/assets.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
/// 静态资源路径常量,统一维护,避免路径字符串散落在业务代码中。
|
||||
///
|
||||
/// 所有路径须与 pubspec.yaml 的 flutter.assets 声明保持一致。
|
||||
/// 新增资源:① 文件放入 assets/ ② pubspec.yaml 声明 ③ 此处加常量。
|
||||
///
|
||||
/// 渲染逻辑(缓存、占位、错误态)由 core/ui/components/ 下的组件负责,不在此处封装。
|
||||
///
|
||||
/// ## 使用
|
||||
/// ```dart
|
||||
/// Image.asset(AppAssets.logo)
|
||||
/// Image.asset(AppAssets.logo, width: 80, fit: BoxFit.cover)
|
||||
/// ```
|
||||
abstract final class AppAssets {
|
||||
AppAssets._();
|
||||
|
||||
// ── 品牌 ──────────────────────────────────────────────────
|
||||
static const logo = 'assets/images/logo.png';
|
||||
static const logoLight = 'assets/images/logo_light.png';
|
||||
|
||||
// ── 占位图 ────────────────────────────────────────────────
|
||||
static const avatarPlaceholder = 'assets/images/avatar_placeholder.png';
|
||||
|
||||
// ── 空状态插图(SVG,引入 flutter_svg 后启用) ─────────────
|
||||
// static const emptyChat = 'assets/svg/empty_chat.svg';
|
||||
// static const emptyContact = 'assets/svg/empty_contact.svg';
|
||||
// static const emptySearch = 'assets/svg/empty_search.svg';
|
||||
|
||||
// ── 动画 ──────────────────────────────────────────────────
|
||||
// static const loading = 'assets/gif/loading.gif';
|
||||
}
|
||||
44
apps/im_app/lib/core/ui/base/icons.dart
Normal file
44
apps/im_app/lib/core/ui/base/icons.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 项目图标常量,统一维护,避免 Icons.xxx 散落在业务代码中。
|
||||
///
|
||||
/// 渲染逻辑(大小、颜色、点击态)由调用方负责,不在此处封装。
|
||||
///
|
||||
/// ## 使用
|
||||
/// ```dart
|
||||
/// Icon(AppIcons.send)
|
||||
/// Icon(AppIcons.send, size: 20, color: Colors.white)
|
||||
/// IconButton(icon: Icon(AppIcons.back), onPressed: ...)
|
||||
/// ```
|
||||
abstract final class AppIcons {
|
||||
AppIcons._();
|
||||
|
||||
// ── 底部导航 ──────────────────────────────────────────────
|
||||
static const chat = Icons.chat_bubble_outline_rounded;
|
||||
static const contact = Icons.people_outline_rounded;
|
||||
static const settings = Icons.settings_outlined;
|
||||
|
||||
// ── 通用操作 ──────────────────────────────────────────────
|
||||
static const back = Icons.arrow_back_ios_new_rounded;
|
||||
static const close = Icons.close_rounded;
|
||||
static const more = Icons.more_horiz_rounded;
|
||||
static const search = Icons.search_rounded;
|
||||
static const add = Icons.add_rounded;
|
||||
|
||||
// ── 聊天输入区 ────────────────────────────────────────────
|
||||
static const send = Icons.send_rounded;
|
||||
static const attach = Icons.attach_file_rounded;
|
||||
static const emoji = Icons.emoji_emotions_outlined;
|
||||
static const camera = Icons.camera_alt_outlined;
|
||||
static const voice = Icons.mic_outlined;
|
||||
|
||||
// ── 用户 / 联系人 ─────────────────────────────────────────
|
||||
static const avatar = Icons.account_circle_outlined;
|
||||
static const addUser = Icons.person_add_outlined;
|
||||
|
||||
// ── 状态反馈 ──────────────────────────────────────────────
|
||||
static const success = Icons.check_circle_outline_rounded;
|
||||
static const warning = Icons.warning_amber_rounded;
|
||||
static const error = Icons.error_outline_rounded;
|
||||
static const info = Icons.info_outline_rounded;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:im_app/app/di/db_provider.dart';
|
||||
import 'package:im_app/data/local/drift/app_database.dart';
|
||||
@@ -45,13 +44,12 @@ class ChatDbTestState {
|
||||
|
||||
@riverpod
|
||||
class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
|
||||
@override
|
||||
ChatDbTestState build() {
|
||||
// 这里就是 onInit
|
||||
final List<TestResult> testResults = List.generate(
|
||||
1000,
|
||||
(i) => TestResult(
|
||||
(i) => TestResult(
|
||||
title: '用户 ${Random().nextInt(9999)}',
|
||||
subtitle: 'uid: ${Random().nextInt(999999)}',
|
||||
duration: '${Random().nextInt(500)}ms',
|
||||
@@ -86,7 +84,7 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
for (var i = 0; i < count; i += chunkSize) {
|
||||
final chunk = List.generate(
|
||||
chunkSize.clamp(0, count - i),
|
||||
(j) => UsersCompanion.insert(
|
||||
(j) => UsersCompanion.insert(
|
||||
uid: Value(i + j),
|
||||
nickname: Value('User ${i + j}'),
|
||||
),
|
||||
@@ -98,13 +96,13 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
// 让出主线程
|
||||
await Future.delayed(Duration.zero);
|
||||
|
||||
debugPrint('已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)');
|
||||
debugPrint(
|
||||
'已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)',
|
||||
);
|
||||
|
||||
// 更新 UI 状态
|
||||
if (ref.mounted) {
|
||||
state = state.copyWith(
|
||||
currentState: '已插入 $completed / $count 条',
|
||||
);
|
||||
state = state.copyWith(currentState: '已插入 $completed / $count 条');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,4 +114,4 @@ class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/ui/base/context_theme_ext.dart';
|
||||
|
||||
@@ -12,7 +13,7 @@ import '../../../../core/ui/base/context_theme_ext.dart';
|
||||
///
|
||||
/// 将 [conversationId] 传给对应的 Riverpod `.family` provider 加载完整会话数据。
|
||||
/// 构造参数保持不变,数据来源从 `extra` 换成 provider 即可。
|
||||
class ChatDetailPage extends StatelessWidget {
|
||||
class ChatDetailPage extends ConsumerWidget {
|
||||
const ChatDetailPage({
|
||||
super.key,
|
||||
required this.conversationId,
|
||||
@@ -23,7 +24,7 @@ class ChatDetailPage extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final s = context.styles;
|
||||
|
||||
return Scaffold(
|
||||
|
||||
@@ -20,8 +20,6 @@ class ChatPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(chatViewModelProvider.notifier);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('聊天')),
|
||||
body: Center(
|
||||
@@ -32,36 +30,48 @@ class ChatPage extends ConsumerWidget {
|
||||
// 切换 Tab:用 go,替换整个历史栈,不可返回
|
||||
AppButton.inverse(
|
||||
label: '切换 Tab(go)',
|
||||
onPressed: () => vm.goToContact(context),
|
||||
onPressed: () =>
|
||||
ref.read(chatViewModelProvider.notifier).goToContact(context),
|
||||
),
|
||||
// 带参数 push:extra 传 Dart Record,适合已有对象的场景
|
||||
AppButton.inverse(
|
||||
label: '有参 push(extra)',
|
||||
onPressed: () => vm.pushChatDetailWithExtra(context),
|
||||
onPressed: () => ref
|
||||
.read(chatViewModelProvider.notifier)
|
||||
.pushChatDetailWithExtra(context),
|
||||
),
|
||||
// 带参数 push:id 内嵌在路径中,适合需要深链接 / 分享的场景
|
||||
AppButton.inverse(
|
||||
label: '有参 push(路径参数)',
|
||||
onPressed: () => vm.pushChatDetailById(context),
|
||||
onPressed: () => ref
|
||||
.read(chatViewModelProvider.notifier)
|
||||
.pushChatDetailById(context),
|
||||
),
|
||||
// 无参 push:压栈,自动显示返回按钮,不切 Tab
|
||||
AppButton.inverse(
|
||||
label: '无参 push',
|
||||
onPressed: () => vm.pushSettingsTheme(context),
|
||||
onPressed: () => ref
|
||||
.read(chatViewModelProvider.notifier)
|
||||
.pushSettingsTheme(context),
|
||||
),
|
||||
// 无参 go:替换历史,切换到对应 Tab,TabBar 可见,不可返回
|
||||
AppButton.inverse(
|
||||
label: '无参 go',
|
||||
onPressed: () => vm.goToSettings(context),
|
||||
onPressed: () => ref
|
||||
.read(chatViewModelProvider.notifier)
|
||||
.goToSettings(context),
|
||||
),
|
||||
AppButton.inverse(
|
||||
label: '测试数据库性能',
|
||||
onPressed: () => vm.goToDatabaseTest(context),
|
||||
onPressed: () => ref
|
||||
.read(chatViewModelProvider.notifier)
|
||||
.goToDatabaseTest(context),
|
||||
),
|
||||
AppButton.secondary(
|
||||
label: '退出登录',
|
||||
fullWidth: false,
|
||||
onPressed: () => vm.logout(),
|
||||
onPressed: () =>
|
||||
ref.read(chatViewModelProvider.notifier).logout(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// 联系人页占位
|
||||
///
|
||||
/// 待 contact 功能开发后替换为实际内容。
|
||||
class ContactPage extends StatelessWidget {
|
||||
class ContactPage extends ConsumerWidget {
|
||||
const ContactPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const Scaffold();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import '../usecases/login_usecase.dart';
|
||||
/// ViewModel Provider 由 `@riverpod` 注解自动生成,不在此文件中。
|
||||
///
|
||||
/// Auth 模块的 DI 链路:Repository → UseCase(按需)。
|
||||
/// app/di/ 只提供 SDK 基础设施(apiConfig / apiClient / socketManager / storageApi),
|
||||
/// app/di/ 只提供 SDK 基础设施(apiConfig / networkSdkApi / socketManager / storageApi),
|
||||
/// 业务模块的 Provider 内聚在 features/{模块}/di/ 下。
|
||||
///
|
||||
/// ```
|
||||
@@ -21,7 +21,7 @@ import '../usecases/login_usecase.dart';
|
||||
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
|
||||
/// → ref.read(socketManagerProvider) ← app/di/ 手动装配
|
||||
/// → ref.read(apiConfigProvider) ← app/di/ 手动装配
|
||||
/// → ref.read(apiClientProvider) ← app/di/ 手动装配
|
||||
/// → ref.read(networkSdkApiProvider) ← app/di/ 手动装配
|
||||
/// → ref.read(storageSdkProvider) ← app/di/ 手动装配
|
||||
/// ```
|
||||
|
||||
@@ -41,7 +41,7 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
// TODO: final secureStorage = ref.read(secureStorageProvider);
|
||||
|
||||
return AuthRepositoryImpl(
|
||||
client: ref.read(networkSdkApiProvider), // 直接注入 ApiClient
|
||||
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
|
||||
onTokenUpdate: (token) {
|
||||
apiConfig.updateToken(token); // 内存(network_sdk)
|
||||
// TODO: secureStorage.saveToken(token); // 持久化(crypto_sdk)
|
||||
|
||||
@@ -29,7 +29,7 @@ part 'login_view_model.g.dart';
|
||||
/// loginViewModelProvider ← @riverpod 自动生成(本文件)
|
||||
/// → ref.read(loginUseCaseProvider) ← di/ 手动装配
|
||||
/// → ref.read(authRepositoryProvider) ← di/ 手动装配
|
||||
/// → ref.read(apiClientProvider) ← app/di/ 手动装配
|
||||
/// → ref.read(networkSdkApiProvider) ← app/di/ 手动装配
|
||||
/// ```
|
||||
///
|
||||
/// ## 数据流位置
|
||||
@@ -56,6 +56,7 @@ class LoginViewModel extends _$LoginViewModel {
|
||||
/// 正式 [login] 成功后同样需要调用 [AuthNotifier.login] 更新守卫状态。
|
||||
Future<void> demoLogin() async {
|
||||
final storageApi = ref.read(storageSdkProvider);
|
||||
|
||||
///TODO: StorageSDKLifeCycle 需要只在主项目暴露
|
||||
final storageLifeCycle = storageApi as StorageSdkLifecycle;
|
||||
ref.read(authNotifierProvider).login();
|
||||
@@ -76,10 +77,9 @@ class LoginViewModel extends _$LoginViewModel {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
final user = await ref.read(loginUseCaseProvider).execute(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
final user = await ref
|
||||
.read(loginUseCaseProvider)
|
||||
.execute(email: email, password: password);
|
||||
|
||||
state = state.copyWith(user: user, isLoading: false);
|
||||
} on FormatException catch (e) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
platform :osx, '11.0'
|
||||
platform :osx, '14.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -38,5 +38,8 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['SWIFT_VERSION'] ||= '6.2'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -496,7 +496,7 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
||||
};
|
||||
name = Release;
|
||||
@@ -511,7 +511,7 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cusotmer.im.imApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/im_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/im_app";
|
||||
};
|
||||
name = Profile;
|
||||
@@ -557,7 +557,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
@@ -580,7 +580,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
@@ -639,7 +639,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
@@ -689,7 +689,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
@@ -713,7 +713,7 @@
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -732,7 +732,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SWIFT_VERSION = 6.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -37,9 +37,13 @@ dependencies:
|
||||
# 网络状态监听
|
||||
connectivity_plus: ^6.1.0
|
||||
|
||||
# JWT 解析(token 过期检测、主动刷新)
|
||||
dart_jsonwebtoken: ^3.3.2
|
||||
|
||||
# 数据库(schema 定义在 im_app,连接/CRUD 封装在 storage_sdk)
|
||||
drift: ^2.22.0
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in New Issue
Block a user