java.lang.IllegalStateException 在 Android Jetpack Compose 中使用状态时
java.lang.IllegalStateException when using State in Android Jetpack Compose
我有 ViewModel
和 Kotlin sealed class 来为 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.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)
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
}
}
}
}
我尝试延迟设置 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 函数中的 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
)
}
}
撰写版本:1.0.0-beta03
如何使用 Compose State
处理密封 class UIState
以便它不会抛出 IllegalStateException?
https://kotlinlang.slack.com/archives/CJLTWPH7S/p1613581738163700.
中讨论了看起来有些相似的问题
我认为该讨论的一些相关部分(来自 Adam Powell)
As for the thread-safety aspects of snapshot state, what you've
encountered is the result of snapshots being transactional.
When a snapshot is taken (and composition does this for you under the
hood) the currently active snapshot is thread-local. Everything that
happens in composition is part of this transaction, and that
transaction hasn't committed yet.
So when you create a new mutableStateOf in composition and then pass
it to another thread, as the GlobalScope.launch in the problem snippet
does, you've essentially let a reference to snapshot state that
doesn't exist yet escape from the transaction.
这里的具体情况略有不同,但我认为关键问题相同。可能不会完全这样做,但至少在这里它通过将 init
的内容移动到新的 getCategories()
方法,然后从 LaunchedEffect
块调用它。 FWIW 我在其他地方所做的事情(同时仍在 init
中调用)是在视图模型中使用 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
)
}
}
因此,在多次尝试解决此问题后,我找到了解决方案。
在 答案的帮助下,我知道快照是事务性的,并且 运行 在 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
}
}
}
我有 ViewModel
和 Kotlin sealed class 来为 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.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)
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
}
}
}
}
我尝试延迟设置 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 函数中的 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
)
}
}
撰写版本:1.0.0-beta03
如何使用 Compose State
处理密封 class UIState
以便它不会抛出 IllegalStateException?
https://kotlinlang.slack.com/archives/CJLTWPH7S/p1613581738163700.
中讨论了看起来有些相似的问题我认为该讨论的一些相关部分(来自 Adam Powell)
As for the thread-safety aspects of snapshot state, what you've encountered is the result of snapshots being transactional.
When a snapshot is taken (and composition does this for you under the hood) the currently active snapshot is thread-local. Everything that happens in composition is part of this transaction, and that transaction hasn't committed yet.
So when you create a new mutableStateOf in composition and then pass it to another thread, as the GlobalScope.launch in the problem snippet does, you've essentially let a reference to snapshot state that doesn't exist yet escape from the transaction.
这里的具体情况略有不同,但我认为关键问题相同。可能不会完全这样做,但至少在这里它通过将 init
的内容移动到新的 getCategories()
方法,然后从 LaunchedEffect
块调用它。 FWIW 我在其他地方所做的事情(同时仍在 init
中调用)是在视图模型中使用 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
)
}
}
因此,在多次尝试解决此问题后,我找到了解决方案。
在
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
}
}
}