collectAsStateWithLifecycle with StateFlow

Quick answer: In an Android Compose screen, expose UI state as a
StateFlowfrom theViewModeland collect it withcollectAsStateWithLifecycle(). It converts the latest flow value to ComposeStateand, by default, collects only while the lifecycle is at leastSTARTED. 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
| Question | Default 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
StateFlowvalue 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
InboxScreenwith fixedInboxUiStatevalues—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.