Jetpack Compose State用密封类UIState抛IllegalStateException问题
你遇到的这个IllegalStateException完全是Compose早期beta版本的快照上下文绑定机制导致的,和你用密封类做状态、Composable里的状态观察逻辑没有任何关系。
在你使用的1.0.0-beta03版本里,SnapshotMutableStateImpl的读写操作是强绑定当前线程关联的快照上下文的。当你在Dispatchers.IO调度的协程里直接修改State值时,如果协程恢复执行的时机刚好没有拿到主线程全局快照的写入权限(也就是报错里说的“快照尚未应用”),就会直接抛出这个异常。
你加delay(10)之后能运行本质是巧合:delay会把协程挂起10毫秒,当协程恢复执行时,刚好已经完成了快照上下文的初始化,拿到了写入权限,根本不是延迟本身解决了问题。而且你会发现同样的逻辑如果换个设备、换个网络场景,哪怕加了delay也可能照样报错,完全不可靠。
至于为什么成功分支不报错、只有错误分支报错:只是因为成功分支的网络请求耗时更长,协程恢复时刚好已经过了快照未初始化的窗口期;错误分支因为网络报错返回太快,协程一恢复就直接往State里写值,刚好踩中了快照系统的报错边界。
1. 把State的写入操作切回主线程
Compose的State写入天生需要在持有全局快照的线程(也就是Android主线程)执行才是安全的,不要在IO线程直接做状态更新。把协程里的状态修改逻辑用withContext(Dispatchers.Main)包裹就能从根本上解决问题:
init { viewModelScope.launch(Dispatchers.IO) { try { val categories = api .getCategory().schemas .map { it.toDomain() } withContext(Dispatchers.Main) { _categoryListState.value = UIState.Success(categories) } } catch (e: Exception) { withContext(Dispatchers.Main) { _categoryListState.value = UIState.Error } } } }
这种方式不需要改你现有的密封类状态结构,也不需要修改Composable里的状态观察逻辑,改动量最小。
2. 用StateFlow做ViewModel层的状态持有
裸mutableStateOf是Compose运行时提供的API,直接放在ViewModel层其实存在层耦合的问题。更符合架构分层的做法是用线程安全的StateFlow承载状态,在Compose层再通过collectAsState()转成Compose State观察:
// ViewModel部分代码 private val _categoryListState = MutableStateFlow<UIState>(UIState.Loading) val categoryListState: StateFlow<UIState> = _categoryListState init { viewModelScope.launch(Dispatchers.IO) { try { val categories = api.getCategory().schemas.map { it.toDomain() } // StateFlow天生线程安全,任意协程线程都可以写入,不会触发快照上下文错误 _categoryListState.value = UIState.Success(categories) } catch (e: Exception) { _categoryListState.value = UIState.Error } } } // Composable观察部分代码 @Composable fun CategoryScreen( viewModel: CategoryListViewModel, onCategoryClicked: (Category) -> Unit ) { val uiState by viewModel.categoryListState.collectAsState() when (uiState) { is CategoryListViewModel.UIState.Error -> CategoryError() is CategoryListViewModel.UIState.Loading -> CategoryLoading() is CategoryListViewModel.UIState.Success -> CategoryList( categories = (uiState as CategoryListViewModel.UIState.Success).categoryList, onCategoryClicked ) } }
这种方案不需要手动做线程切换,也不会和Compose的快照系统产生上下文绑定的冲突,是现在官方推荐的ViewModel和Compose层的状态通信方式。
注:你现有Composable里的状态观察写法、密封类的状态结构本身没有任何问题,只要保证状态写入的上下文是正确的,完全可以正常使用。
内容的提问来源于stack exchange,提问作者Vladuken

