Skip to content
AB

StateFlow and Immutable UI State in Compose

Expose one lifecycle-aware state stream and update it atomically as asynchronous work completes.

Ashish BhakhandPublished

Compose works best when rendering is a function of immutable state.

Code
private val _state = MutableStateFlow(DashboardState())
val state: StateFlow<DashboardState> = _state.asStateFlow()

fun refresh() = viewModelScope.launch {
    _state.update { it.copy(isRefreshing = true, error = null) }
    runCatching { repository.refresh() }
        .onSuccess { data -> _state.update { it.copy(data = data) } }
        .onFailure { error -> _state.update { it.copy(error = error.toUiError()) } }
    _state.update { it.copy(isRefreshing = false) }
}

Use update so concurrent changes transform the latest state rather than a stale captured copy.

Collect with lifecycle awareness in Compose. A screen in the background should not keep performing UI collection work unnecessarily.

Do not put mutable lists inside an otherwise immutable data class. Copy collections when ownership changes, and give stable keys to lazy lists.

Separate long-lived state from one-time effects. A SharedFlow or channel can carry navigation and messages, while StateFlow always exposes the current renderable truth.

For derived values, prefer pure properties or combine source flows in the ViewModel. Recomputing expensive mappings inside every composable recomposition makes performance hard to reason about.

← Back to all writing