在 Android Jetpack Compose 中使用 State 时出现 java.lang.IllegalStateException

Vla*_*ken 6 android kotlin kotlin-coroutines android-jetpack-compose

ViewModel使用 Kotlin 密封类为 UI 提供不同的状态。此外,我使用androidx.compose.runtime.State对象来通知 UI 状态的变化。

如果错误MyApi发生的要求,我把UIState.FailureMutableState对象,然后我得到IllegalStateException

 java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
        at androidx.compose.runtime.snapshots.SnapshotKt.readError(Snapshot.kt:1524)
        at androidx.compose.runtime.snapshots.SnapshotKt.current(Snapshot.kt:1764)
        at androidx.compose.runtime.SnapshotMutableStateImpl.setValue(SnapshotState.kt:797)
        at com.vladuken.compose.ui.category.CategoryListViewModel$1.invokeSuspend(CategoryListViewModel.kt:39)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Run Code Online (Sandbox Code Playgroud)

ViemModel 代码:

@HiltViewModel
class CategoryListViewModel @Inject constructor(
    private val api: MyApi
) : ViewModel() {

    sealed class UIState {
        object Loading : UIState()
        data class Success(val categoryList: List<Category>) : UIState()
        object Error : UIState()
    }

    val categoryListState: State<UIState>
        get() = _categoryListState
    private val _categoryListState =
        mutableStateOf<UIState>(UIState.Loading)

    init {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val categories = api
                    .getCategory().schemas
                    .map { it.toDomain() }
                _categoryListState.value = UIState.Success(categories)

            } catch (e: Exception) {
                //this does not work
                _categoryListState.value = UIState.Error
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

我试图延迟设置 UIState.Error - 它奏效了,但我认为这不是正常的解决方案:

viewModelScope.launch(Dispatchers.IO) {
            try {
                val categories = api
                    .getCategory().schemas
                    .map { it.toDomain() }
                _categoryListState.value = UIState.Success(categories)

            } catch (e: Exception) {
                //This works 
                delay(10)
                _categoryListState.value = UIState.Error
            }
        }
Run Code Online (Sandbox Code Playgroud)

我在 Composable 函数中观察 State 对象如下:

@Composable
fun CategoryScreen(
    viewModel: CategoryListViewModel,
    onCategoryClicked: (Category) -> Unit
) {
    when (val uiState = viewModel.categoryListState.value) {
        is CategoryListViewModel.UIState.Error -> CategoryError()
        is CategoryListViewModel.UIState.Loading -> CategoryLoading()
        is CategoryListViewModel.UIState.Success -> CategoryList(
            categories = uiState.categoryList,
            onCategoryClicked
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

撰写版本: 1.0.0-beta03

如何UIState使用 Compose处理密封类State,使其不会抛出 IllegalStateException?

小智 50

解决这个问题的三种方法是

    1. 调用可组合项中启动的效果块中的方法
    1. 或者在使用 withContext(Dispatchers.Main) 设置 mutableState 的值时将 Context 设置为 Dispatchers.Main
    1. 或者将 viewModel 中的可变状态更改为 mutableState 流,并使用 composable 中的collectAsState() 将其收集为状态。

  • 非常简洁和准确。 (2认同)

Joh*_*lly 17

https://kotlinlang.slack.com/archives/CJLTWPH7S/p1613581738163700中有一个关于看起来有些类似问题的讨论。

我认为该讨论的一些相关部分(来自亚当·鲍威尔)

至于快照状态的线程安全方面,您遇到的是快照事务性的结果。

当拍摄快照时(组合在幕后为您完成此操作),当前活动的快照是线程本地的。组合中发生的所有事情都是该事务的一部分,并且该事务尚未提交。

因此,当您在组合中创建一个新的 mutableStateOf 并将其传递给另一个线程时(如问题片段中的 GlobalScope.launch 所做的那样),您实际上已经让对尚不存在的快照状态的引用从事务中逃逸了。

这里的确切场景略有不同,但我认为关键问题相同。可能不会完全按照这种方式来做,但至少在这里它通过将initin 的内容移动到新getCategories()方法中来工作,然后从LaunchedEffectblock 中调用该方法。FWIW 我在其他地方所做的事情(同时仍然在 中调用init)是StateFlow在视图模型中使用,然后collectAsState()在 Compose 代码中调用。

@Composable
fun CategoryScreen(
    viewModel: CategoryListViewModel,
    onCategoryClicked: (Category) -> Unit
) {
    LaunchedEffect(true) {
        viewModel.getCategories()
    }

    when (val uiState = viewModel.categoryListState.value) {
        is CategoryListViewModel.UIState.Error -> CategoryError()
        is CategoryListViewModel.UIState.Loading -> CategoryLoading()
        is CategoryListViewModel.UIState.Success -> CategoryList(
            categories = uiState.categoryList,
            onCategoryClicked
        )
    }
}
Run Code Online (Sandbox Code Playgroud)


Vla*_*ken 13

因此,经过多次尝试解决这个问题,我找到了解决方案。在/sf/answers/4682450951/答案的帮助下,我发现快照是事务性的并在 ui 线程上运行 - 更改调度程序有帮助:

viewModelScope.launch(Dispatchers.IO) {
            try {
                val categories = api
                    .getCategory().schemas
                    .map { it.toDomain() }
                _categoryListState.value = UIState.Success(categories)

            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    _categoryListState.value = UIState.Error
                }
            }
        }
Run Code Online (Sandbox Code Playgroud)