collectAsStateWithLifecycle with StateFlow

Quick answer: In an Android Compose screen, expose UI state as a StateFlow from the ViewModel and collect it with collectAsStateWithLifecycle(). It converts the latest flow value to Compose State and, by default, collects only while the lifecycle is at least STARTED. Pass the resulting value to a stateless UI.

The Android state guide recommends collectAsStateWithLifecycle() for collecting Flow in Android apps. It avoids doing collection work while the screen is stopped, while Compose recomposes readers when a new state value arrives during an active lifecycle.

Add the Android lifecycle Compose artifact

dependencies {
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
}

The API belongs to androidx.lifecycle:lifecycle-runtime-compose; check the current state documentation for the version compatible with your project. For platform-agnostic Compose code, use collectAsState() instead, because collectAsStateWithLifecycle() is Android-specific.

Expose read-only StateFlow from the ViewModel

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

data class InboxUiState(
    val isLoading: Boolean = true,
    val messages: List<MessageUi> = emptyList(),
)

class InboxViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(InboxUiState())
    val uiState = _uiState.asStateFlow()

    fun onRefreshComplete(messages: List<MessageUi>) {
        _uiState.update { it.copy(isLoading = false, messages = messages) }
    }
}

The UI receives a read-only stream; mutation remains an explicit event handled by the owner. This preserves a single source of truth and follows the state-hoisting guidance.

Collect at the screen boundary

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun InboxRoute(
    viewModel: InboxViewModel = viewModel(),
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    InboxScreen(
        uiState = uiState,
        onRefresh = viewModel::refresh,
    )
}

@Composable
fun InboxScreen(
    uiState: InboxUiState,
    onRefresh: () -> Unit,
) {
    // Render uiState and send user actions upward.
}

For StateFlow, the returned state starts with the flow’s current value. New emissions update it and recompose locations that read it while the lifecycle is active, as described in the API reference.

Know the lifecycle boundary

QuestionDefault behavior
When does collection begin?At Lifecycle.State.STARTED
What happens when the screen stops?Collection is stopped until it becomes active again
What is the first rendered StateFlow value?The flow’s current value
Can the minimum state change?Yes, pass minActiveState when STARTED is not suitable

Avoid manually wiring onStart/onStop flow collection around a composable. The lifecycle guidance recommends the lifecycle-aware API so the collection follows the screen naturally.

Common mistakes

Collecting an event as durable UI state

StateFlow is ideal for the current screen model: loading, content, selection, and errors that remain visible. One-time navigation, snackbars, and analytics need an event/effect design; do not assume every flow emission should be rendered forever. See Compose side effects for effect ownership.

Collecting the same flow in several unrelated children

Collect once at a route or other state-owner boundary, then pass values and callbacks to children. This makes previews and tests simpler and avoids hiding ownership. State hoisting with real examples shows that split.

Using it in shared Compose code

This lifecycle-aware API is Android-only. In shared multiplatform UI, use collectAsState() with an explicit initial value for an ordinary Flow, then let the platform layer own lifecycle policy.

Verify behavior

  • Start with a non-loading StateFlow value and verify it renders immediately.
  • Rotate the device and confirm the ViewModel remains the state owner.
  • Background and foreground the app while a stream updates; the screen should render the latest active value when collection resumes.
  • Test the stateless InboxScreen with fixed InboxUiState values—loading, empty, content, and error.

For state that must survive system process recreation, collection alone is not persistence. Combine the appropriate SavedStateHandle or repository source with the Compose state-saving strategy.

FAQ

Do I need an initial value with StateFlow?

No. StateFlow always has a current value, and collectAsStateWithLifecycle() uses it initially. An ordinary Flow overload requires an initial value.

Should I use RESUMED instead of STARTED?

Only when the product truly requires collection exclusively in the foreground-interactive state. STARTED is the API default and is suitable for most screen UI state.