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

42

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

如果在MyApi请求中发生错误,我将UIState.Failure放入MutableState对象中,然后我会收到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)

ViewModel 代码:

@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
            }
        }
    }

}

我尝试推迟设置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
            }
        }

我如下所示在可组合函数中观察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
        )
    }
}

Compose 版本: 1.0.0-beta03

如何使用 Compose State 处理密封类 UIState,以避免抛出 IllegalStateException?

3个回答

41

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

    1. 在您的组合中调用已启动的效果块中的方法
    1. 或者在使用withContext(Dispatchers.Main)设置mutableState的值时将上下文设置为Dispatchers.Main
    1. 或者将viewModel中的可变状态更改为mutableState flow,并在组合中使用collectAsState()将其作为状态进行收集。

2
非常简洁和准确。 - Cyber Avater
1
第三个选项更适合于需要使用Dispachers.IO进行数据库操作的情况。准确的答案。 - alpertign
@Neo,你能解释一下原因吗? - undefined

16

https://kotlinlang.slack.com/archives/CJLTWPH7S/p1613581738163700中有关于类似问题的讨论。

我认为以下是讨论中一些相关的部分(来自Adam Powell)

对于快照状态的线程安全方面,你遇到的是快照事务性造成的结果。

当一个快照被创建时(并且组合函数会在内部执行这个操作),当前活动的快照是线程本地的。组合过程中发生的所有事情都是这个事务的一部分,但这个事务还没有提交。

所以当你在组合函数中创建一个新的mutableStateOf然后将其传递给另一个线程,就像问题片段中的GlobalScope.launch一样,你实际上是让不存在的快照状态引用逃逸出事务。

这里的确切场景略有不同,但我认为它们存在相同的关键问题。也许不会完全按照这种方式进行操作,但至少在这里通过将init的内容移动到名为getCategories()的新方法中,然后从LaunchedEffect块中调用它,实现了预期的功能。值得一提的是,在其他情况下,我在视图模型中使用StateFlow,然后在Compose代码中调用collectAsState()

@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
        )
    }
}

13

所以,在更多尝试修复此问题之后,我找到了一个解决方案。 在https://dev59.com/vVEG5IYBdhLWcg3wYcDg#66892156的帮助下,我发现快照是事务性的并且在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
                }
            }
        }

不要使用 try catch,而是使用 flow{ }.catch{}。 - Morpheus
这对我不起作用。我收到以下错误:java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied - Raj Narayanan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接