繁体   English   中英

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

[英]java.lang.IllegalStateException when using State in Android Jetpack Compose

我有 Kotlin 密封 class 的ViewModel为 UI 提供不同的状态。 另外,我使用androidx.compose.runtime.State object 通知 UI state 的变化。

如果MyApi请求发生错误,我将UIState.Failure放入MutableState object 然后我得到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)

Viem模型代码:

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

我在 Composable function 中观察到 State object 如下:

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

撰写版本: 1.0.0-beta03

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

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

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

在 kotlinlang.slack.com/archives/CJLTWPH7S/p1613581738163700 中有一个关于看起来有点相似的问题的讨论。

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

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

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

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

确切的情况在这里有点不同,但我认为同样的关键问题。 可能不会完全这样做,但至少在这里它通过将init的内容移动到新的getCategories()方法来工作,然后从LaunchedEffect块调用该方法。 FWIW 我在其他地方做的这种情况(同时仍在init中调用)是在视图 model 中使用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
        )
    }
}

因此,经过更多尝试解决此问题后,我找到了解决方案。 https://stackoverflow.com/a/66892156/13101450答案的帮助下,我知道快照是事务性的并且在 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
                }
            }
        }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM