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 页面从开始到消失花费了大概 一秒的时间。
- Compose 函数重组,显示loading
- 协程启动,获取到最新的状态,更新state 状态
- 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 在大多数不显示的时候