为何在协程中构建Jetpack Compose AnnotatedString无法响应状态变化?
问题描述
我有一个会根据状态变化计算样式的AnnotatedString,字符串中单个单词的颜色会随timecode的状态变化而改变:
- 把构建逻辑放在常规Compose上下文时,字符串表现符合预期,但由于字符串很长,主线程构建会导致屏幕加载卡顿。
- 把构建逻辑移到
LaunchedEffect的协程中,AnnotatedString能正常构建并显示,但不再响应状态变化,颜色无法跟上timecode的更新。
原代码
val audioState by viewModel.audioStateFlow.collectAsState() val annotatedString = remember { mutableStateOf(AnnotatedString(text = "")) } fun dynamicColor(timeCode: Long): Color { println(timeCode) <<==== 移到LaunchedEffect里构建时看不到这个输出 println(audioState.timeCode) <<===== 这个也看不到 return if (timeCode < audioState.timeCode) progressColour else plainTextColour } LaunchedEffect(speakerAndTranscriptsList.isNotEmpty()) { // 在这里构建字符串能正常显示,但不会响应状态变化 } annotatedString.value = buildAnnotatedString { viewModel.speakerAndTranscriptsList.forEach { pushStyle( SpanStyle( color = speakerColour, fontSize = 15.sp ) ) appendLine(it.speaker.name) pop() it.transcripts.forEach { transcript -> pushStyle( SpanStyle( color = dynamicColor(transcript.start) <<<<<<======= 这里在主线程构建会卡顿!, fontSize = 15.sp ) ) pushStringAnnotation( tag = "timecode", annotation = transcript.start.toString() ) append(transcript.content) pop() } appendLine() appendLine() } } ClickableText( text = annotatedString.value, onClick = { offset -> // 点击逻辑 } ) }
原因分析
- LaunchedEffect不响应状态变化:你给
LaunchedEffect设置的触发条件是speakerAndTranscriptsList.isNotEmpty(),它只会在列表从空变为非空时执行一次协程逻辑。后续audioState.timeCode更新时,触发条件未变化,协程不会重新执行,自然无法更新字符串样式。而且协程内部没有监听audioState的变化,就算手动触发也感知不到状态更新。 - 主线程构建卡顿:每次
audioState.timeCode变化时,Compose都会重新执行buildAnnotatedString逻辑,长字符串的大量样式计算和拼接操作会阻塞主线程,导致UI卡顿。
解决方法
使用produceState异步构建AnnotatedString,它能在后台协程中执行耗时的构建逻辑,同时自动监听依赖的状态变化,既避免主线程卡顿,又能响应状态更新。
修改后的代码
val audioState by viewModel.audioStateFlow.collectAsState() val speakerAndTranscriptsList = viewModel.speakerAndTranscriptsList // 使用produceState异步构建AnnotatedString val annotatedString by produceState(initialValue = AnnotatedString("")) { // 监听audioState.timeCode和speakerAndTranscriptsList的变化,任一更新都会触发重新构建 snapshotFlow { Pair(audioState.timeCode, speakerAndTranscriptsList) } .collect { (currentTimeCode, transcripts) -> if (transcripts.isEmpty()) return@collect // 在协程中构建字符串,不阻塞主线程 val newAnnotatedString = buildAnnotatedString { transcripts.forEach { pushStyle( SpanStyle( color = speakerColour, fontSize = 15.sp ) ) appendLine(it.speaker.name) pop() it.transcripts.forEach { transcript -> val textColor = if (transcript.start < currentTimeCode) progressColour else plainTextColour pushStyle( SpanStyle( color = textColor, fontSize = 15.sp ) ) pushStringAnnotation( tag = "timecode", annotation = transcript.start.toString() ) append(transcript.content) pop() } appendLine() appendLine() } } // 更新状态值,通知Compose重组UI value = newAnnotatedString } } ClickableText( text = annotatedString, onClick = { offset -> // 点击逻辑 } )
代码说明
produceState创建可观察状态,初始值为空字符串。snapshotFlow将audioState.timeCode和speakerAndTranscriptsList包装成流,任一依赖变化时,流会发射新值,触发重新构建。- 字符串构建逻辑在协程中执行,避免阻塞主线程,解决卡顿问题。
- 依赖更新时,重新计算样式并构建新的
AnnotatedString,更新produceState的value后,Compose会自动重组ClickableText展示最新样式。
内容的提问来源于stack exchange,提问作者John
相关产品推荐
相关产品推荐

