18 KiB
18 KiB
状态控制设计
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<List<ChannelInfo>> // 频道列表(低频,按需请求)
├── clients: StateFlow<List<ClientInfo>> // 客户端列表(高频,人员变动全量刷新)
├── channelClients: StateFlow<Map<String, List<ClientInfo>>> // 按频道索引(由 clients 派生)
├── selfClientId: StateFlow<Int> // 自身客户端 ID
├── currentChannelId: StateFlow<String> // 当前频道 ID
├── serverInfo: StateFlow<ServerInfo?> // 服务器信息
├── unreadCounts: StateFlow<Map<String, Int>> // 未读消息计数
├── 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<ServerScreenState>
│ ├── address / nickname / password // 输入字段
│ ├── connectState: ConnectState // 连接按钮状态
│ │ ├── IDLE 空闲,等待用户输入
│ │ ├── CONNECTING 连接中
│ │ ├── SUCCESS 连接成功(触发导航)
│ │ ├── FAILED 连接失败
│ │ └── TIMEOUT 连接超时
│ └── errorMessage / validationErrors
│
├── connectionState: StateFlow<ConnectionState?>
│ ├── null 未进入频道列表页
│ ├── Connected 已连接
│ ├── Disconnecting 断开中
│ ├── Reconnecting 重连中(attempt, reason)
│ └── Disconnected 已断开(reason, wasKicked)
│
├── serverInfo: StateFlow<ServerInfo?>
├── kickReason: StateFlow<String>
├── pokeNotification / showPokeNotification
├── themeMode: StateFlow<ThemeMode>
└── recents: StateFlow<List<RecentConnection>>
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<SyncState>
│ ├── Unsynced 已连接但尚无完整数据(初始状态)
│ ├── Syncing 正在调用 ListChannels / ListClients
│ ├── Synchronized 列表基线可供 UI 使用
│ └── SyncFailed 同步失败
│
├── channels: StateFlow<List<ChannelInfo>> // Repository 引用(低频)
├── clients: StateFlow<List<ClientInfo>> // Repository 引用(高频)
├── channelClients: StateFlow<Map<String, List<ClientInfo>>> // Repository 引用
├── selfClientId: StateFlow<Int> // Repository 引用
├── currentChannelId: StateFlow<String> // Repository 引用
│
├── expandedChannelIds: StateFlow<Set<String>> // 本地状态(初始同步时自动展开有成员的频道)
├── hasUnreadMessage: StateFlow<Boolean> // 本地状态(当前频道外有新消息)
│
├── switchState: StateFlow<ChannelSwitchState>
│ ├── Idle 空闲
│ ├── Requesting 正在发送 ClientMove
│ ├── WaitingServerEvent 等待 OnClientMoved 确认
│ └── Failed 切换失败
│
├── showPasswordDialog / pendingSwitchChannel
└── channelDetailInfo / showChannelDetailCard
频道树构建(UI 层): UI 直接从源 StateFlow 实时计算,不维护中间状态:
// 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<List<ChatMessage>> // 当前会话消息
├── sendState: StateFlow<MessageSendState>
│ ├── 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<VoiceState>
│ ├── Idle 空闲(可 PTT)
│ ├── Transmitting 发送中(PTT 按下)
│ └── Blocked 受阻(原因:未连接/未入频道/权限/断开)
│
├── isMuted: StateFlow<Boolean> // 默认 true
├── isTransmitting: StateFlow<Boolean>
├── speakerEnabled: StateFlow<Boolean>
├── denoiseEnabled: StateFlow<Boolean>
├── showVoiceCard: StateFlow<Boolean>
├── outputDevice: StateFlow<VoiceOutputDevice>
├── inputVolume / outputVolume: StateFlow<Float>
├── isSelfSpeaking: StateFlow<Boolean>
└── speakingClients: StateFlow<Map<Int, Long>>
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 派生计算 │
└─────────────────────────────────────────┘
数据流规则:
- Repository 持有底层数据。
channels低频更新(连接时请求),clients高频更新(每次人员变动全量刷新) - ViewModel 读取 Repository 的 StateFlow 引用,仅管理本地 UI 状态(expandedChannelIds、hasUnreadMessage、switchState 等)
- UI 通过
collectAsState()订阅 StateFlow,使用remember实时派生计算(频道树)。不维护中间 combine 状态 - 事件 从 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() 本身是幂等操作,无副作用 |