1

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?

1 Answers1

0

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.

Megh
  • 831
  • 2
  • 12
  • As you said both styles will provide the same result (the current value of the `MutableStateFlow`), but when I use property delegation it makes (1) `is UiState.Success>` return true but casting to (2) `uiState as UiState.Success>` throws an execption. In the stack trace `com.dainghia.brewfootball.ui.UiState$Loading cannot be cast to com.dainghia.brewfootball.ui.UiState$Success` looks like the value at (1) is the new value and when (2) is executing it receives the old value – dainghia.tran Jun 05 '23 at 06:37
  • The issue occurs because the type information is not propagated correctly when using property delegation with collectAsState() inside the items block of LazyColumn. By accessing the value directly using .value, you obtain the actual value and ensure the correct type information is available for smart casting. Basically, in your case, its not directly related to property delegation or using .value but its related to the items block of LazyColumn, in which the type information is not propagated correctly to the compiler. – Megh Jun 05 '23 at 09:42
  • I got it, thank you – dainghia.tran Jun 06 '23 at 15:54