The Power of stateIn in ViewModel

Building Declarative UI State with Kotlin Flow.

Image created by AI

In Jetpack Compose, we often see ViewModels that manually manage UI state, call load() from init, and update several properties whenever data changes.

This approach works, but it can introduce unnecessary boilerplate. As an application grows, keeping multiple pieces of state synchronized becomes increasingly difficult.

Kotlin Flow offers a more declarative alternative. By combining Flow operators with stateIn, we can define how UI state is derived from its inputs and let the stream handle updates.

The Hidden Cost of Manual State Updates

Consider a typical ViewModel:

class MyViewModel(
private val repository: MyRepository
) : ViewModel() {

private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

init {
load()
}

private fun load() {
viewModelScope.launch {
val items = repository.loadItems()

_uiState.update {
it.copy(items = items)
}
}
}
}

This is a perfectly valid implementation. It is easy to understand, especially when loading data involves a one-time operation.

However, the ViewModel is responsible for several things:

  • Starting the initial load.
  • Updating the state after loading.
  • Preserving unrelated state fields.
  • Keeping the UI state consistent as more inputs are added.

As the screen becomes more complex, these responsibilities can lead to repetitive state-management code.

What if the repository already exposes a Flow of items? Do we still need to collect it manually and copy every update into another state holder?

Often, we do not.

Do We Really Need to Call load()?

A common Compose pattern is:

LaunchedEffect(Unit) {
viewModel.load()
}

Another option is to call load() from the ViewModel’s init block.

Both approaches have legitimate uses. A user-triggered refresh, a one-time operation, or a request that depends on a specific event may need an explicit command.

But when the screen simply reflects data that is already represented by a Flow, manually initiating and synchronizing every update may be unnecessary.

The important distinction is between commands and state:

  • Commands express what should happen.
  • State describes what is currently true.

stateIn is particularly useful when UI state can be derived from an existing Flow rather than maintained through repeated imperative updates.

Let stateIn Turn a Flow into UI State

Suppose the repository exposes a stream of items:

interface MyRepository {
val items: Flow<List<Item>>
}

The ViewModel can derive its UI state directly:

class MyViewModel(
repository: MyRepository
) : ViewModel() {

val uiState: StateFlow<UiState> = repository.items
.map { items ->
UiState(items = items)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState()
)
}

Notice what disappeared:

  • No private MutableStateFlow for manually updating the same UI state.
  • No init block to start collection.
  • No explicit load() call just to copy repository emissions into UI state.
  • No manual synchronization between repository data and the UI model.

Instead, the relationship is expressed directly:

Repository Flow

↓

map

↓

UiState

↓

stateIn

↓

StateFlow<UiState>

Whenever the repository emits a new list, the Flow produces a corresponding UiState.

stateIn converts the upstream Flow into a hot StateFlow, shares its latest value, and provides an initial value. The ViewModel exposes a state stream that the UI can observe.

This is the key benefit: the code describes how state is derived, instead of manually synchronizing it after every change.

Multiple Inputs, One Consistent UI State

The benefits become clearer when a screen depends on more than one input.

Imagine a list screen with a search query. The visible items depend on both the repository data and the query.

With manual state updates, the ViewModel must coordinate changes to both.

With Flow, the dependency is explicit:

class MyViewModel(
repository: MyRepository
) : ViewModel() {

private val query = MutableStateFlow("")

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()
)

fun onQueryChanged(query: String) {
this.query.value = query
}
}

The UI action updates only the query input. It does not directly mutate the public uiState.

The derived state updates automatically when either input changes:

  • The repository emits new items.
  • The user changes the query.

There is no need to remember every place where the visible list must be recalculated.

The dependency is defined once, in code.

Keep Commands Explicit, State Declarative

This does not mean every operation should be represented as a Flow.

For example, submitting a form, deleting an item, or refreshing data in response to an explicit user action may require a command handled by the ViewModel.

The useful design principle is to avoid using imperative commands merely to keep derived state synchronized.

If UI state depends on several observable inputs, express that relationship with operators such as map and combine, then use stateIn when the UI needs a StateFlow.

The result is a clearer separation:

  • Inputs represent data and user intent.
  • Flow operators describe the relationships between inputs.
  • stateIn exposes the derived result as a state holder.
  • Compose renders the state.

This makes the ViewModel easier to reason about because dependencies are visible rather than hidden inside a sequence of state mutations.

Choose the Right Sharing Strategy

stateIn requires a sharing strategy. One common choice is:

SharingStarted.WhileSubscribed(5_000)

This starts upstream collection when the first subscriber appears and stops it five seconds after the last subscriber disappears.

The timeout can help avoid stopping and restarting the upstream stream during brief gaps in UI subscription.

However, stopping collection is not the same as guaranteeing that a network request will be repeated when the screen returns. Whether a request runs again depends on how the upstream Flow is implemented.

Also, stateIn does not fetch data by itself. It shares and retains values emitted by its upstream Flow.

Choose the sharing strategy based on the lifecycle and behavior you want, not simply because it is a common pattern.

Mutable State Still Has Its Place

MutableStateFlow remains useful for inputs and state that genuinely needs to be changed imperatively.

For example:

private val query = MutableStateFlow("")

The query is an input controlled by user interaction. Updating it is appropriate.

The derived uiState, on the other hand, does not need to be independently mutable when it can be calculated from the repository data and query.

A practical rule is:

  • Use MutableStateFlow for mutable inputs.
  • Use Flow operators to derive state from those inputs.
  • Use stateIn to expose the resulting stream as StateFlow.

This is not about eliminating mutable state. It is about putting mutability where it belongs.

Conclusion: Let stateIn Do the State Management

stateIn is more than a convenient way to convert a Flow into a StateFlow. Used with operators such as map and combine, it helps make the ViewModel’s state model declarative.

Instead of manually collecting data, copying values, and synchronizing related fields, we can express the dependencies between inputs and UI state directly.

The approach is especially valuable when a screen’s state is derived from multiple observable sources.

Keep commands explicit. Keep state declarative. Let Flow define the dependencies, and use stateIn to expose the result as UI state whenever that is the right fit.

The Beauty of stateIn in ViewModel

Enjoyed this article? Here’s how you can support my work:

  • 👏 Clap (up to 50 times!) if you found this insightful.
  • 👤 Follow me and turn on notifications (🔔) so you never miss the next deep dive.
  • 📧 Subscribe to get my latest stories delivered directly to your inbox.

You can find all my articles on Android architecture, Jetpack Compose, and Kotlin in this curated reading list.


The Power of stateIn in ViewModel was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.