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

为何在协程中构建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 ->
      // 点击逻辑
    }
  )
}
原因分析
  1. LaunchedEffect不响应状态变化:你给LaunchedEffect设置的触发条件是speakerAndTranscriptsList.isNotEmpty(),它只会在列表从空变为非空时执行一次协程逻辑。后续audioState.timeCode更新时,触发条件未变化,协程不会重新执行,自然无法更新字符串样式。而且协程内部没有监听audioState的变化,就算手动触发也感知不到状态更新。
  2. 主线程构建卡顿:每次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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 06:53:22