The Beauty of stateIn in ViewModel
Build UI state by describing how it is derived, not by manually synchronizing values.

One of the most useful things about stateIn in a ViewModel is that it lets us describe how UI state is derived from data, rather than manually updating it whenever something changes.
Consider the usual approach:
class MyViewModel(
private val repository: MyRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
init {
viewModelScope.launch {
repository.items.collect { items ->
_uiState.value = UiState(items = items)
}
}
}
}
This works. But we have to maintain two properties, launch a coroutine, collect the Flow, and manually assign each new value to _uiState.
Now look at the same idea with stateIn:
class MyViewModel(
private val repository: MyRepository
) : ViewModel() {
val uiState: StateFlow<UiState> =
repository.items
.map { items ->
UiState(items = items)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState()
)
}
The code is shorter, but that is not the most important benefit.
The real benefit is that the relationship between the data and the UI state is explicit.
The UI state is derived from the repository’s Flow. When the upstream data changes, the transformation produces a new UiState. We do not need a separate mutable state holder or a collector whose only job is to copy one value into another.
This becomes even more valuable when a screen depends on multiple inputs:
val uiState: StateFlow<UiState> = combine(
repository.items,
query
) { items, query ->
UiState(
items = items.filter { it.matches(query) },
query = query
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState()
)
The UI state now expresses its dependencies directly. Both the repository data and the search query contribute to the result.
With manual updates, we would need to decide where and when to update the state whenever either input changes. With combine, that relationship is already encoded in the Flow pipeline.
There is also a useful separation of responsibilities:
- The repository provides data.
- The ViewModel derives the UI state.
- The UI observes the resulting
StateFlow.
Each layer has a clear role.
stateIn also lets us control when the upstream Flow is collected. With SharingStarted.WhileSubscribed(5_000), collection stops after the last subscriber disappears for five seconds and resumes when a subscriber returns. This can avoid unnecessary upstream work, depending on how the source Flow is implemented.
Of course, stateIn is not always the answer. For a value that represents an independent, mutable piece of UI state, MutableStateFlow remains a perfectly reasonable choice. And if an upstream Flow must continue running without UI subscribers, a different sharing policy may be appropriate.
The point is not to eliminate mutable state everywhere.
It is to stop manually synchronizing state that can be derived from other state.
When UI state is a function of its inputs, let the Flow pipeline express that function. Use stateIn to expose the result as a shared StateFlow.