# 状态控制设计 ## 1. 整体架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ Go Bridge (TSBridge) │ │ onConnected / onDisconnected / onTextMessage / onClientEnter │ │ onClientLeave / onClientMoved / onKicked / onVoiceData / onPoked│ └──────────────────────────────┬──────────────────────────────────┘ │ JNI 回调 ▼ ┌──────────────────────────────────────────────────────────────────┐ │ ServerViewModel (协调者) │ │ connectionState / serverInfo / kickReason / pokeNotification │ │ 拥有其他 ViewModel 引用,分发事件 │ └──────┬────────────┬────────────┬────────────┬────────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ ChannelVM │ │ ChatVM │ │ VoiceVM │ │ Repository│ │ 频道列表 │ │ 文字聊天 │ │ 语音通信 │ │ (单例) │ │ 频道切换 │ │ 消息归档 │ │ PTT控制 │ │ 数据仓库 │ │ 未读状态 │ │ 送达确认 │ │ 说话检测 │ │ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Compose UI (StateFlow collectAsState) │ │ NavGraph → ChannelListScreen / ChatScreen / KickedScreen │ └──────────────────────────────────────────────────────────────────┘ ``` ## 2. Repository(单例数据仓库) 所有 ViewModel 共享的底层数据源。ViewModel 读取 Repository 的 StateFlow,事件处理通过 Repository 方法更新。 ``` Repository (object) ├── channels: StateFlow> // 频道列表(低频,按需请求) ├── clients: StateFlow> // 客户端列表(高频,人员变动全量刷新) ├── channelClients: StateFlow>> // 按频道索引(由 clients 派生) ├── selfClientId: StateFlow // 自身客户端 ID ├── currentChannelId: StateFlow // 当前频道 ID ├── serverInfo: StateFlow // 服务器信息 ├── unreadCounts: StateFlow> // 未读消息计数 ├── activeChatKey: String? // 当前查看的会话 └── messageArchives: ConcurrentHashMap // 消息归档 ``` **数据更新频率设计原则**: | 数据 | 变动频率 | SDK 事件支持 | 更新策略 | |------|---------|-------------|---------| | 频道列表 | 极低频 | 无事件 | 连接时全量请求一次,之后按需刷新(超5分钟过期检测) | | 客户端列表 | 高频 | Enter/Leave/Move | 每次事件全量刷新 `refreshClientList()` | | 频道树 | — | — | 不维护中间状态,UI 层直接从 channels + clients 实时计算 | **关键方法**: - `performInitialSync()` — 首次同步(串行请求频道列表、客户端列表、自身 ID) - `refreshClientList()` — 全量刷新客户端列表(人员变动时调用,高频) - `updateBaseline()` — 原子提交基线数据 - `clearSession()` — 清理会话(断开连接时调用) - `archiveMessage()` — 归档消息 - `confirmMessageDelivery()` — 匹配回显确认送达 ## 3. ViewModel 状态明细 ### 3.1 ServerViewModel — 连接与事件协调 **职责**:管理连接生命周期、接收 Go Bridge 回调并分发给其他 ViewModel。 ``` ServerViewModel ├── state: StateFlow │ ├── address / nickname / password // 输入字段 │ ├── connectState: ConnectState // 连接按钮状态 │ │ ├── IDLE 空闲,等待用户输入 │ │ ├── CONNECTING 连接中 │ │ ├── SUCCESS 连接成功(触发导航) │ │ ├── FAILED 连接失败 │ │ └── TIMEOUT 连接超时 │ └── errorMessage / validationErrors │ ├── connectionState: StateFlow │ ├── null 未进入频道列表页 │ ├── Connected 已连接 │ ├── Disconnecting 断开中 │ ├── Reconnecting 重连中(attempt, reason) │ └── Disconnected 已断开(reason, wasKicked) │ ├── serverInfo: StateFlow ├── kickReason: StateFlow ├── pokeNotification / showPokeNotification ├── themeMode: StateFlow └── recents: StateFlow> ``` **Bridge 回调分发**: | 回调 | 分发目标 | |------|---------| | `onConnected` | `channelViewModel.performInitialSync()` → 导航到频道列表 | | `onDisconnected` | `Repository.clearSession()` + `channelViewModel.clearChannels()` + `chatViewModel.clearMessages()` | | `onTextMessage` | `chatViewModel.handleTextMessage()` + `channelViewModel.onTextMessage()` | | `onClientEnter` | `channelViewModel.handleClientEnter()` | | `onClientLeave` | `channelViewModel.handleClientLeave()` | | `onClientMoved` | `channelViewModel.handleClientMoved()` | | `onKicked` | `Repository.clearSession()` + 所有 ViewModel 清理 → 导航到 KickedScreen | | `onVoiceData` | `voiceViewModel.handleVoiceData()` | | `onPoked` | `serverViewModel.handlePoked()` | **ViewModel 引用**(由 NavGraph LaunchedEffect 设置): ``` serverViewModel.channelViewModel = channelViewModel serverViewModel.chatViewModel = chatViewModel serverViewModel.voiceViewModel = voiceViewModel ``` ### 3.2 ChannelViewModel — 频道列表与切换 **职责**:管理同步状态、频道切换状态机、展开状态、未读指示。频道树不在 ViewModel 中维护,由 UI 层实时计算。 ``` ChannelViewModel ├── syncState: StateFlow │ ├── Unsynced 已连接但尚无完整数据(初始状态) │ ├── Syncing 正在调用 ListChannels / ListClients │ ├── Synchronized 列表基线可供 UI 使用 │ └── SyncFailed 同步失败 │ ├── channels: StateFlow> // Repository 引用(低频) ├── clients: StateFlow> // Repository 引用(高频) ├── channelClients: StateFlow>> // Repository 引用 ├── selfClientId: StateFlow // Repository 引用 ├── currentChannelId: StateFlow // Repository 引用 │ ├── expandedChannelIds: StateFlow> // 本地状态(初始同步时自动展开有成员的频道) ├── hasUnreadMessage: StateFlow // 本地状态(当前频道外有新消息) │ ├── switchState: StateFlow │ ├── Idle 空闲 │ ├── Requesting 正在发送 ClientMove │ ├── WaitingServerEvent 等待 OnClientMoved 确认 │ └── Failed 切换失败 │ ├── showPasswordDialog / pendingSwitchChannel └── channelDetailInfo / showChannelDetailCard ``` **频道树构建(UI 层)**: UI 直接从源 StateFlow 实时计算,不维护中间状态: ```kotlin // ChannelListScreen val tree = remember(channels, clients, expandedChannelIds) { channelViewModel.buildChannelTree(channels, clients, expandedChannelIds, unreadStates) } ``` **频道数据更新策略**: - 频道列表是静态数据,SDK 不提供频道增删改事件 - 连接时全量请求一次(`performInitialSync`),之后按需刷新(超5分钟过期检测) - 人员变动事件(Enter/Leave/Move)只刷新客户端列表,不碰频道列表 - 如果管理员创建/删除了频道,用户下次切换频道时自动检测过期并刷新 **频道切换状态机**: ``` 用户点击频道 │ ▼ Idle ──→ Requesting ──→ WaitingServerEvent ──→ Idle │ │ │ │ ▼ ▼ │ Failed 超时 → Failed │ │ └───────────┘ 用户重试/取消 ``` **事件处理**: | 事件 | 处理 | |------|------| | `handleClientEnter()` | `Repository.refreshClientList()` 全量刷新(高频) | | `handleClientLeave()` | `Repository.refreshClientList()` 全量刷新(高频) | | `handleClientMoved()` | 区分自己/他人,全量刷新 + 更新 currentChannelId(高频) | | `onTextMessage()` | 设置 hasUnreadMessage 标记(仅当 targetId != currentChannelId) | ### 3.3 ChatViewModel — 文字聊天 **职责**:管理消息列表、发送状态、送达确认。 ``` ChatViewModel ├── messages: StateFlow> // 当前会话消息 ├── sendState: StateFlow │ ├── Idle 空闲 │ ├── Sending 发送中 │ └── Failed 发送失败 │ ├── currentTargetMode: Int // 1=私聊, 2=频道, 3=服务器 ├── currentTargetId: Long // 目标 ID └── deliveryTimeoutJob // 送达确认超时 ``` **消息生命周期**: ``` 用户输入 │ ▼ sendMessage() │ ├── 1. 创建 PENDING 消息 → Repository.archiveMessage() ├── 2. TSBridge.sendTextMessage() │ ├── 成功 → 等待回显(10秒超时) │ └── 失败 → 标记 FAILED │ ▼ 收到 OnTextMessage 回显 │ ├── Repository.confirmMessageDelivery() 匹配 PENDING │ ├── 匹配 → 标记 SENT(送达确认) │ └── 不匹配 → 正常归档(他人消息) │ ▼ 超时未确认 → 标记 FAILED ``` **会话管理**: - `enterChat()` — 设置 activeChatKey,加载消息,清除未读 - `leaveChat()` — 清除 activeChatKey,重置发送状态 - `clearMessages()` — 清空消息列表(断开连接时) ### 3.4 VoiceViewModel — 语音通信 **职责**:管理 PTT、静音、语音状态、说话检测。 ``` VoiceViewModel ├── voiceState: StateFlow │ ├── Idle 空闲(可 PTT) │ ├── Transmitting 发送中(PTT 按下) │ └── Blocked 受阻(原因:未连接/未入频道/权限/断开) │ ├── isMuted: StateFlow // 默认 true ├── isTransmitting: StateFlow ├── speakerEnabled: StateFlow ├── denoiseEnabled: StateFlow ├── showVoiceCard: StateFlow ├── outputDevice: StateFlow ├── inputVolume / outputVolume: StateFlow ├── isSelfSpeaking: StateFlow └── speakingClients: StateFlow> ``` **PTT 状态机**: ``` Idle ──→ startTransmit() ──→ Transmitting ──→ stopTransmit() ──→ Idle │ │ │ 前置检查失败 │ 异常 ▼ ▼ Blocked ←──────────────────────────┘ │ └── clearBlocked() ──→ Idle ``` ## 4. 状态生命周期 ### 4.1 连接建立 ``` 用户点击连接 │ ▼ ServerViewModel.connect() │ connectState = CONNECTING ▼ TSBridge.connect() (IO 线程) │ ▼ onConnected 回调 │ ├── Repository 记录连接 ├── ChannelVM.performInitialSync() │ ├── syncState = Syncing │ ├── Repository.performInitialSync() (串行请求) │ ├── syncState = Synchronized │ └── autoExpandChannelsWithClients() │ ├── ConnectionService.start() (前台服务保活) ├── connectState = SUCCESS └── connectionState = Connected │ ▼ NavGraph 导航到 CHANNEL_LIST ``` ### 4.2 正常断开 ``` 用户点击断开 │ ▼ ServerViewModel.disconnect() │ ├── ConnectionService.stop() ├── reconnectJob?.cancel() ├── connectionState = Disconnecting ├── voiceViewModel.stopVoice() ├── TSBridge.disconnect() │ ├── Repository.clearSession() │ ├── channels/clients = empty │ ├── selfClientId = 0 │ ├── currentChannelId = "0" │ ├── channelClients = empty │ └── messageArchives.clear() │ ├── channelViewModel.clearChannels() │ ├── syncState = Unsynced │ ├── expandedChannelIds = empty │ ├── unreadStates = empty │ └── switchState = Idle │ ├── chatViewModel.clearMessages() │ ├── messages = empty │ └── sendState = Idle │ ├── connectionState = null └── connectState = IDLE │ ▼ NavGraph 导航到 SERVER_CONFIG ``` ### 4.3 被踢出 ``` onKicked 回调 │ ├── ConnectionService.stop() ├── reconnectJob?.cancel() ├── voiceViewModel.stopVoice() ├── Repository.clearSession() ├── channelViewModel.clearChannels() │ ├── kickReason = reason └── connectionState = Disconnected(wasKicked=true) │ ▼ NavGraph 导航到 KICKED ``` ### 4.4 网络断开(自动重连已禁用) ``` onDisconnected 回调 │ ├── 检查 connectionState │ ├── Disconnecting → 忽略(主动断开) │ └── 其他 → 继续 │ ├── voiceViewModel.onDisconnected() │ └── voiceState = Blocked("连接已断开") │ ├── ConnectionService.stop() ├── Repository.clearSession() ├── channelViewModel.clearChannels() │ └── connectionState = Disconnected(wasKicked=false) │ ▼ NavGraph 导航到 SERVER_CONFIG ``` ## 5. 状态流向图 ``` ┌──────────────────────────────┐ │ Repository │ │ channels (低频) / clients (高频) │ └──────┬───────────────────────┘ │ StateFlow(只读引用) ┌────────────┼────────────────┐ ▼ ▼ ▼ ┌────────────┐ ┌──────────┐ ┌──────────┐ │ ChannelVM │ │ ChatVM │ │ VoiceVM │ │ 同步状态 │ │ 消息管理 │ │ 语音控制 │ │ 切换状态机 │ │ 送达确认 │ │ PTT │ │ 展开/未读 │ │ │ │ │ └─────┬──────┘ └────┬─────┘ └────┬─────┘ │ StateFlow │ StateFlow │ StateFlow ▼ ▼ ▼ ┌─────────────────────────────────────────┐ │ Compose UI Layer │ │ collectAsState() → remember 派生计算 │ └─────────────────────────────────────────┘ ``` **数据流规则**: 1. **Repository** 持有底层数据。`channels` 低频更新(连接时请求),`clients` 高频更新(每次人员变动全量刷新) 2. **ViewModel** 读取 Repository 的 StateFlow 引用,仅管理本地 UI 状态(expandedChannelIds、hasUnreadMessage、switchState 等) 3. **UI** 通过 `collectAsState()` 订阅 StateFlow,使用 `remember` 实时派生计算(频道树)。不维护中间 combine 状态 4. **事件** 从 ServerViewModel(Bridge 回调)→ ViewModel 方法 → Repository 更新 → StateFlow 自动通知 UI ## 6. 导航状态控制 NavGraph 监听 `connectionState` 变化,自动处理导航: ``` connectionState 变化 │ ├── Disconnected(wasKicked=true) → navigate(KICKED) ├── Disconnected(wasKicked=false) → navigate(SERVER_CONFIG) ├── Connected (当前在 KICKED) → navigate(CHANNEL_LIST) └── 其他 → 不处理 ``` **页面与 ViewModel 生命周期**: - ViewModel 作用域绑定到 NavBackStackEntry - 从 CHANNEL_LIST 导航到 CHAT 时,ChannelVM 不销毁(在返回栈中) - 从 CHAT 返回 CHANNEL_LIST 时,ChannelVM 状态保持 - 如果系统回收 ViewModel(内存不足),`LaunchedEffect` 检测 `syncState != Synchronized` 并重新触发同步 ## 7. 已知状态问题与防护 | 问题 | 防护措施 | |------|---------| | ViewModel 被系统回收后 syncState 回到 Unsynced | ChannelListScreen 的 `LaunchedEffect` 自动重新同步 | | 详情按钮快速连点 | ViewModel 层 500ms 防抖 | | `leaveChat()` 被调用多次(Compose 重组) | `leaveChat()` 本身是幂等操作,无副作用 |