Compose MutableState 多协程异步更新-- 探讨Compose 状态数据结构使用

458 阅读4分钟

MutableState 读异常

背景 首页启动的时候需要后台服务准备好,后台服务没有准备好的时候加载一个loading 页面。 代码结构如下

loading
----BootLoadingScreen.kt
----BootLoadingScreen.kt

class BootLoadingViewModel : ViewModel() {
    private val _showLoading = mutableStateOf(true)
    val showLoadingState = _showLoading
    
    init {
       initData()
    }

    viewModelScope.launch(Dispatchers.IO) {
       RepositoryDreamview.vehicleStatus
          .takeWhile { _showLoading.value }
          .collect { vehicleStatus ->
             if (vehicleStatus.isOk()) {
                 _showLoading.value = false
             }
          }
}

运行时error

java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
翻译下就是 快照还没有创建或者快照创建了但是没有提交的时候读取了 State

Compose 的重组声明周期和快照提交

BootLoadingViewModel 是在调用BootLoadingScreen 的时候创建的。按说启动速度比较快的。

fun BootLoadingScreen1(bootLoadingViewModel: BootLoadingViewModel = viewModel()) {

MutableState 是建立在快照的基础上的。Compose 的每次重组都要基于状态提交一次快照。

Recomposer.kt

private inline fun <T> composing(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?,
    block: () -> T
): T {
    val snapshot = Snapshot.takeMutableSnapshot(
        readObserverOf(composition), writeObserverOf(composition, modifiedValues)
    )
    try {
        return snapshot.enter(block)
    } finally {
        applyAndCheck(snapshot)
    }
}

private fun applyAndCheck(snapshot: MutableSnapshot) {
    try {
        val applyResult = snapshot.apply()
        if (applyResult is SnapshotApplyResult.Failure) {
            error(
                "Unsupported concurrent change during composition. A state object was " +
                    "modified by composition as well as being modified outside composition."
            )
            // TODO(chuckj): Consider lifting this restriction by forcing a recompose
        }
    } finally {
        snapshot.dispose()
    }
}

那大概率是Compose 重组没有完成,快照没有提交,BootLoadingViewModel的init 启动速度比较快,这个时候读取了 _showLoading 状态。

验证下我们的猜想,加下日志。

2024-08-08 15:05:40.103 4004-4004 BootLoadingScreen start loading 页启动
2024-08-08 15:05:40.108 4004-4004 initData start ViewModel 的init执行 2024-08-08 15:05:40.110 4004-4004 initData end ViewModel 的init执行 结束
2024-08-08 15:05:40.112 4004-4108 collect _showLoading showLoading 收集
2024-08-08 15:05:40.113 4004-4004 showLoadingState true showLoading 状态更新,读state 状态,这个地方发生异常
2024-08-08 15:05:40.167 4004-4004 allllllllll end Compose 函数执行结束 完成重组
2024-08-08 15:05:40.319 4004-4004 DisposableEffect init Compose 的Disposable 开始执行
2024-08-08 15:05:41.281 4004-4004 LaunchedEffect init 2024-08-08 15:05:41.299 4004-4004 BootLoadingScreen start

从日志上看确实是compose 函数重组没有完成的时候读取state 状态导致异常。

解决办法

主线程收集数据状态

从ViewMode 代码中可以看到,State 转态的收集放到了IO 线程中去执行,这就导致View的重组的主线程和IO线程产生了异步,转态有了时间差。第一个办法就是放到主线程中收集状态。

viewModelScope.launch(Dispatchers.IO) {

正常启动从日志上看 状态的读取也在Compose 重组完成后

15:21:10.468 I BootLoadingScreen start
15:21:10.474 I initData start
15:21:10.475 I initData end
15:21:10.478 I BootLoadingContent showLoadingState true
15:21:10.529 I allllllllll end Compose 重组完成
15:21:10.741 I DisposableEffect init
15:21:10.742 I BootLoadingContent DisposableEffect init
15:21:11.382 I collect _showLoading
15:21:11.383 I update _showLoading 读取状态
15:21:11.491 I BootLoadingContent showLoadingState false
15:21:11.499 I BootLoadingContent DisposableEffect end

放到LaunchEffect 中执行

使用Compose 的副作用 LaunchEffect 从上面的日志中也可以看到,Compose 重组完成后才会调用副作用函数

LaunchedEffect(Unit) {
    Timber.tag("SnapShot").i("LaunchedEffect init")
     bootLoadingViewModel.initData()
}

使用MutableStateFlow 替换 MutableSate

使用MutableStateFlow 本质上是Flow, 并没有和快照系统关联,所以我们可以在多个协程中更新Flow 状态 在Compose 函数中collectAsStateWithLifecycle 转换flow 到state

val showLoadingState = bootLoadingViewModel.showLoadingFlow.collectAsStateWithLifecycle()

启动速度优化

从日志上看loading 页面从开始到消失花费了大概 一秒的时间。

  1. Compose 函数重组,显示loading
  2. 协程启动,获取到最新的状态,更新state 状态
  3. Compose 函数重组,loading 消失

15:21:10.468 I BootLoadingScreen start
15:21:11.491 I BootLoadingContent showLoadingState false

思考 如果协程状态提前启动,执行到 Loading Compose 函数的时候,showLoadingState false 已经false , 直接跳过,省略过这一秒。

15:47:37.884 I BootLoadingScreen start
15:47:37.889 I initData start
15:47:37.891 I initData end
15:47:37.893 I BootLoadingContent showLoadingState false loading Compose 直接消失

Compose 是否显示的状态 放在parent 的ViewModel 中更早启动,可以优化一些启动时间,特别是 Compose 在大多数不显示的时候