'java.lang.IllegalStateException when using State in Android Jetpack Compose
I have ViewModel
with Kotlin sealed class to provide different states for UI. Also, I use androidx.compose.runtime.State
object to notify UI about changes in state.
If error on MyApi
request occurs, I put UIState.Failure
to MutableState
object and then I get 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)
ViemModel code:
@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
}
}
}
}
I tried to delay setting UIState.Error - and it worked, but I don't think it is normal solution:
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
}
}
I observe State object in Composable function as follows:
@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 Version : 1.0.0-beta03
How to process sealed class UIState
with Compose State
so that it doesn't throw IllegalStateException?
Solution 1:[1]
So, after more attempts of fixing this issue I found a solution. With help of https://stackoverflow.com/a/66892156/13101450 answer I've get that snapshots are transactional and run on ui thread - changing dispatcher helped:
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
}
}
}
Solution 2:[2]
There's a discussion about what looks like somewhat similar issue in https://kotlinlang.slack.com/archives/CJLTWPH7S/p1613581738163700.
Some relevant parts of that discussion I think (from 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.
The exact scenario is a little different here but I think same key issue. Probably wouldn't do it exactly this way but at least here it worked by moving contents of init
in to new getCategories()
method which is then called from LaunchedEffect
block. FWIW what I've done elsewhere in case like this (while still invoking in init
) is using StateFlow
in view model and then call collectAsState()
in Compose code.
@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
)
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Vladuken |
Solution 2 | theapache64 |