首次推送
This commit is contained in:
+431
@@ -0,0 +1,431 @@
|
||||
# 状态控制设计
|
||||
|
||||
## 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 实时计算,不维护中间状态:
|
||||
```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<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 派生计算 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**数据流规则**:
|
||||
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()` 本身是幂等操作,无副作用 |
|
||||
Reference in New Issue
Block a user