You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Compose Multiplatform中使用Server Sent Events更新UI时出现列表循环累加与UI重复重组问题

Compose Multiplatform中使用Server Sent Events更新UI时出现列表循环累加与UI重复重组问题

我帮你梳理下当前代码里的几个核心问题,导致列表无限累加、UI疯狂重组:

  1. ViewModel实例化错误:你在App Composable里直接用MainViewModel()作为默认参数,这会导致每次Compose重组时都会创建一个全新的ViewModel实例。每个新实例都会启动一个SSE订阅,多个订阅同时推送数据,列表自然会疯狂膨胀,UI也会无限重组。
  2. 列表处理逻辑过于简单:你在ViewModel里只是把收到的Note直接追加到当前列表末尾,既没有去重,也没有处理服务端可能的全量推送逻辑。如果你的SSE服务是每次连接就推送所有历史Note,或者重复推送数据,列表会无限增长,UI也会跟着不停刷新。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 14:25:27