Search code examples
kotlinandroid-jetpack-composekotlin-coroutineskotlin-stateflowcomposable

Type casting exception when collecting StateFlow in Composable function


I'm facing an issue when collecting StateFlow in Composable function. Although I checked its type, an exception is thrown indicating that cannot cast. It runs correctly in the first call to iewModel.fetchFixtures(), but it throws exception on the next time this method is invoked. Did I misimplement something?

@Composable
fun Home(
    ...
) {
    ...
    val uiState by viewModel.fixtureStateFlow.collectAsState()
    when (uiState) {
        is UiState.Loading -> Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            CircularProgressIndicator()
        }

        is UiState.Success<List<Fixture>> -> LazyColumn(
            modifier.fillMaxSize(),
            contentPadding = PaddingValues(16.dp)
        ) {
            items((uiState as UiState.Success<List<Fixture>>).data) { fixture -> // line 53
                FixtureItem(fixture = fixture) {
                    println("Clicked")
                }
            }
        }
    }
}

ViewModel

@HiltViewModel
class HomeViewModel @Inject constructor(
    ...
) : ViewModel() {
    ...
    private val _fixturesUiState = MutableStateFlow<UiState<List<Fixture>>>(UiState.Loading)
    val fixturesUiState = _fixturesUiState.asStateFlow()

    fun fetchFixtures(date: LocalDate) {
        viewModelScope.launch(coroutineExceptionHandler) {
            _fixturesUiState.value = UiState.Loading
            _fixturesUiState.value = UiState.Success(GetFixtures(repository, date).execute())
        }
    }
}

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    class Success<T>(
        val data: T
    ) : UiState<T>()
}

Stacktrace

FATAL EXCEPTION: main
Process: com.dainghia.brewfootball, PID: 21288
java.lang.ClassCastException: com.dainghia.brewfootball.ui.UiState$Loading cannot be cast to com.dainghia.brewfootball.ui.UiState$Success
at com.dainghia.brewfootball.ui.main.composable.FixtureListKt$FixtureList$2$1.invoke(FixtureList.kt:53)
at com.dainghia.brewfootball.ui.main.composable.FixtureListKt$FixtureList$2$1.invoke(FixtureList.kt:52)
at androidx.compose.foundation.lazy.LazyListItemProviderKt$rememberLazyListItemProvider$1$itemProviderState$1.invoke(LazyListItemProvider.kt:54)
at androidx.compose.foundation.lazy.LazyListItemProviderKt$rememberLazyListItemProvider$1$itemProviderState$1.invoke(LazyListItemProvider.kt:53)
at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2200)
at androidx.compose.runtime.DerivedSnapshotState.currentRecord(DerivedState.kt:161)
at androidx.compose.runtime.DerivedSnapshotState.getCurrentValue(DerivedState.kt:231)
at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.recordInvalidation(SnapshotStateObserver.kt:523)
at androidx.compose.runtime.snapshots.SnapshotStateObserver.drainChanges(SnapshotStateObserver.kt:66)
at androidx.compose.runtime.snapshots.SnapshotStateObserver.access$drainChanges(SnapshotStateObserver.kt:38)
at androidx.compose.runtime.snapshots.SnapshotStateObserver$applyObserver$1.invoke(SnapshotStateObserver.kt:45)
at androidx.compose.runtime.snapshots.SnapshotStateObserver$applyObserver$1.invoke(SnapshotStateObserver.kt:43)
at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot(Snapshot.kt:1768)
at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot(Snapshot.kt:1779)
at androidx.compose.runtime.snapshots.SnapshotKt.access$advanceGlobalSnapshot(Snapshot.kt:1)
at androidx.compose.runtime.snapshots.Snapshot$Companion.sendApplyNotifications(Snapshot.kt:568)
at androidx.compose.ui.platform.GlobalSnapshotManager$ensureStarted$1.invokeSuspend(GlobalSnapshotManager.android.kt:46)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7884)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.AndroidUiFrameClock@b9756e1, StandaloneCoroutine{Cancelling}@eeebc06, AndroidUiDispatcher@b9412c7]

I tried to make this change in Composable function:

val uiState = fixtureStateFlow.collectAsState().value

And this works.

Could you please help me understand the root cause of this issue?


Solution

  • You can modify your code in this manner to fix the above mentioned crash:

    val uiState by viewModel.fixtureStateFlow.collectAsState()
    uiState.let{ state->
        when (state) {
            is UiState.Loading -> Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
    
            is UiState.Success<List<Fixture>> -> LazyColumn(
                modifier.fillMaxSize(),
                contentPadding = PaddingValues(16.dp)
            ) {
                val fixtures = state.data
                items(fixtures) { fixture -> // line 53
                    FixtureItem(fixture = fixture) {
                        println("Clicked")
                    }
                }
            }
        }
    }
    

    Now in context of your question: Why there is no crash while using this-> val uiState = fixtureStateFlow.collectAsState().value, the answer is:

    1. val value by viewmodel.viewmodelvalue.collectAsState(): In this style, you're using property delegation. The by keyword delegates the collection of the MutableStateFlow to the collectAsState() function, which returns a State object. The value property of the State object gives you access to the current value of the MutableStateFlow.

    2. val value = viewmodel.viewmodelvalue.collectAsState().value: Here, you're explicitly calling the value property on the State object returned by collectAsState(). This directly gives you the current value of the MutableStateFlow.

    Both styles will provide you with the same result, which is the current value of the MutableStateFlow. However, using the property delegation style (first style) is generally considered more concise and readable. It abstracts away the details of calling collectAsState() and directly provides access to the value through the value property.