Compose Multiplatform中使用Server Sent Events更新UI时出现列表循环累加与UI重复重组问题
Compose Multiplatform中使用Server Sent Events更新UI时出现列表循环累加与UI重复重组问题
我帮你梳理下当前代码里的几个核心问题,导致列表无限累加、UI疯狂重组:
- ViewModel实例化错误:你在
AppComposable里直接用MainViewModel()作为默认参数,这会导致每次Compose重组时都会创建一个全新的ViewModel实例。每个新实例都会启动一个SSE订阅,多个订阅同时推送数据,列表自然会疯狂膨胀,UI也会无限重组。 - 列表处理逻辑过于简单:你在ViewModel里只是把收到的Note直接追加到当前列表末尾,既没有去重,也没有处理服务端可能的全量推送逻辑。如果你的SSE服务是每次连接就推送所有历史Note,或者重复推送数据,列表会无限增长,UI也会跟着不停刷新。
- State收集方式不够安全:用
collectAsState()没有结合生命周期管理,在组件处于后台时可能还在持续订阅,浪费资源的同时也可能加重UI重组问题。
分步修复方案
1. 先解决最致命的ViewModel实例化问题
把Composable里的ViewModel默认参数改成用viewModel()函数,它会帮你维护全局唯一的ViewModel实例,不会重复创建:
@Composable @Preview fun App( viewModel: MainViewModel = viewModel() // 替换成viewModel() ) { // 改用collectAsStateWithLifecycle,结合生命周期自动管理订阅 val uiState = viewModel.uiState.collectAsStateWithLifecycle().value MaterialTheme { Column( modifier = Modifier .safeContentPadding() .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { LazyColumn() { items( items = uiState, key = { note -> note.id } ){ note -> Text( modifier = Modifier.fillMaxWidth(), text = note.title ) } } } } }
注意要先添加依赖(如果没加的话):
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
2. 修复列表的更新逻辑,避免无限累加
我们需要根据Note的唯一id来做去重和更新,确保同一个Note只会在列表里出现一次,新的Note追加,已有Note则替换:
private fun getStream(){ viewModelScope.launch { ktorNetwork.getStream() .filterNotNull() // 过滤掉null的Note,减少无效操作 .distinctUntilChangedBy { it.id } // 按id过滤重复推送的同一个Note .collect { newNote -> _uiState.update { currentList -> // 先移除已存在的同idNote(处理更新场景) val updatedList = currentList.filterNotNull().toMutableList() val existingIndex = updatedList.indexOfFirst { it.id == newNote.id } if (existingIndex != -1) { updatedList[existingIndex] = newNote } else { updatedList.add(newNote) } updatedList } } } }
同时建议把_uiState的类型改成MutableStateFlow<List<Note>>(提前过滤null值),减少UI层的空判断:
private val _uiState = MutableStateFlow<List<Note>>(emptyList()) val uiState = _uiState.asStateFlow()
3. 优化SSE连接的稳定性(可选)
如果你的SSE服务端会偶尔断开连接,客户端会自动重连,这时候每次重连可能会收到全量数据,我们可以在重连时先重置列表:
// 修改KtorNetwork的getStream方法,添加重连逻辑和重置信号 fun getStream(): Flow<Note?> = callbackFlow { var isConnected = true while (isConnected) { try { // 重连前发送一个null信号,告诉客户端重置列表 trySend(null) client.sse( URL, request = { headers.append(HttpHeaders.Accept, "text/event-stream") }, deserialize = { typeInfo, jsonString -> val serializer = Json.serializersModule.serializer(typeInfo.kotlinType!!) Json.decodeFromString(serializer, jsonString)!! }) { incoming.collect { event: TypedServerSentEvent<String> -> val note: Note? = deserialize<Note>(event.data) trySend(note) } } } catch (e: Exception) { e.printStackTrace() // 重连间隔,避免频繁重试 delay(2000) } } awaitClose { isConnected = false } }
然后在ViewModel里处理这个重置信号:
private fun getStream(){ viewModelScope.launch { ktorNetwork.getStream() .collect { note -> if (note == null) { // 收到重置信号,清空列表 _uiState.value = emptyList() } else { _uiState.update { currentList -> val updatedList = currentList.toMutableList() val existingIndex = updatedList.indexOfFirst { it.id == note.id } if (existingIndex != -1) { updatedList[existingIndex] = note } else { updatedList.add(note) } updatedList } } } } }
额外建议
- 检查你的SSE服务端逻辑:尽量做增量推送(只推新增/修改/删除的Note),而不是持续推送全量数据,这样客户端的处理会更高效。
- 可以给Note类实现
equals和hashCode方法(或用data class),确保distinctUntilChanged类的操作能正确生效。
内容来源于stack exchange
相关产品推荐
相关产品推荐

