stateIn and shareIn
stateIn and shareIn are Flow operators that convert a cold, upstream Flow into a hot, shared stream — a StateFlow or SharedFlow respectively — that multiple collectors can observe without each triggering their own independent execution of the producer. This matters enormously in Android apps where a Room query or a repository combining several sources is expensive to run repeatedly: without sharing, every screen (or every recomposition that re-collects) would re-run the same query from scratch. Both operators take a CoroutineScope to run in, a SharingStarted policy that controls when the upstream producer starts and stops, and (for stateIn) an initialValue to emit before the first real value arrives.
Cricket analogy: stateIn/shareIn are like turning a private ball-by-ball radio feed into the stadium's shared public address broadcast, so every fan's transistor radio (collector) hears the same live commentary instead of each fan somehow triggering their own personal commentator to restart from scratch.
The choice between stateIn and shareIn comes down to the shape of the data. stateIn produces a StateFlow<T>, which always holds exactly one current value (conflated) — perfect for UI state like 'the current list of todos' where only the latest value matters and there must always be a value present, hence the required initialValue. shareIn produces a SharedFlow<T>, which can be configured to replay zero or more past values to new subscribers and does not require an initial value — better suited to event streams like one-off snackbar messages or navigation events, where you may want zero replay (so late subscribers don't see stale events) or where multiple past emissions matter.
Cricket analogy: stateIn is like a scoreboard that always shows one current score (conflated, always present), fitting 'runs scored so far,' while shareIn is like a highlights replay queue that can hold onto several recent boundary replays for late-arriving fans, fitting one-off wicket alerts rather than a single running total.
SharingStarted Policies
SharingStarted.Eagerly starts collecting the upstream immediately when stateIn/shareIn is called and never stops, even with zero collectors — appropriate for data that should always be warm and ready, at the cost of running even when no one is watching. SharingStarted.Lazily starts the upstream on the first collector and keeps it running forever after that, even if all collectors disappear. SharingStarted.WhileSubscribed(stopTimeoutMillis) starts on the first collector and stops stopTimeoutMillis after the last collector disappears — the standard choice for ViewModel UI state, since it stops expensive work (like a live Room query or a location stream) when the screen is backgrounded, but the short delay (commonly 5000ms) prevents the flow from restarting immediately during a brief configuration change like device rotation.
Cricket analogy: Eagerly is a floodlight rig switched on before the ground even opens and left running all night; Lazily is a floodlight that switches on at first pitch and never switches off even after the crowd leaves; WhileSubscribed(5000) is a floodlight that switches off shortly after the last spectator leaves but stays lit through a brief rain-delay evacuation and return.
class TodoViewModel(private val repository: TodoRepository) : ViewModel() {
// Hot UI state: always has a current value, shared across all collectors
val uiState: StateFlow<List<TodoItem>> = repository.observeTodos()
.map { todos -> todos.sortedByDescending { it.createdAt } }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
// One-off events: no replay, so a late subscriber doesn't replay old snackbars
private val _events = MutableSharedFlow<TodoEvent>()
val events: SharedFlow<TodoEvent> = _events.asSharedFlow()
fun onDeleteFailed() {
viewModelScope.launch {
_events.emit(TodoEvent.ShowSnackbar("Failed to delete item"))
}
}
}Why WhileSubscribed(5000) Is the Default Recommendation
The Android team recommends WhileSubscribed(5000) as the default policy for ViewModel-exposed UI state because it balances two competing concerns: stopping expensive upstream work when truly unneeded (saving battery and resources when the app is backgrounded), and not thrashing the upstream producer during transient events. When the device rotates, the Activity is briefly destroyed and recreated, which momentarily drops the collector count to zero; a stopTimeoutMillis of zero would cancel and immediately restart the upstream (re-running a Room query or reconnecting a socket) on every rotation. A 5-second grace period lets the new Activity's composable reconnect before the upstream is actually torn down.
Cricket analogy: WhileSubscribed(5000) is like a ground's floodlight crew waiting five seconds after the players briefly leave the field for a drinks break before actually powering down, since restarting the whole lighting rig for a two-minute break would waste more than just leaving it on; a device rotation is exactly that kind of brief break.
Think of WhileSubscribed(5000) like a taxi that waits 5 seconds after its last passenger steps out before driving away, in case they just stepped out briefly to grab something. If a new passenger (a recreated Activity after rotation) shows up within that window, the taxi (and its running meter — the upstream Flow) never had to stop and restart.
A common mistake is calling stateIn with SharingStarted.Eagerly for data driven by a Room query or GPS stream tied to a specific screen. Because Eagerly never stops on its own, the query or sensor keeps running even after the user navigates away from that screen and the ViewModel is still alive (e.g. cached in the back stack), wasting battery and resources. Eagerly is best reserved for lightweight, app-wide data that genuinely should always be warm, like a small config flag.
- stateIn converts a cold Flow into a hot StateFlow that always holds a single current, conflated value.
- shareIn converts a cold Flow into a hot SharedFlow, useful for events, with configurable replay.
- Both require a CoroutineScope and a SharingStarted policy; stateIn additionally requires an initialValue.
- SharingStarted.WhileSubscribed(5000) is the recommended default for ViewModel UI state.
- The 5-second grace period in WhileSubscribed prevents the upstream from restarting on brief configuration changes like rotation.
- Eagerly keeps the upstream running forever regardless of collectors and should be reserved for lightweight, always-needed data.
Practice what you learned
1. What is the key structural difference between the output of stateIn and shareIn?
2. Why does stateIn require an initialValue parameter while shareIn does not?
3. Why is SharingStarted.WhileSubscribed(5000) commonly recommended over WhileSubscribed(0) for ViewModel state?
4. What is a risk of using SharingStarted.Eagerly for a screen-specific Room query exposed via stateIn?
5. Which SharingStarted policy is most appropriate for a lightweight, app-wide value that should always be warm regardless of any active collector?
Was this page helpful?
You May Also Like
Kotlin Flow Fundamentals
Flow is Kotlin's asynchronous stream type for emitting multiple sequential values over time, built on coroutines, and central to reactive state management in Android apps.
Coroutines Basics in Android
Kotlin coroutines provide a lightweight, structured way to write asynchronous, non-blocking code on Android using suspend functions, coroutine builders, dispatchers, and scopes.
ViewModel Basics
What Android's ViewModel class does, how it survives configuration changes, and how to create and scope ViewModels correctly in a Compose app.
Lifecycle-Aware Coroutines
Lifecycle-aware coroutine APIs like repeatOnLifecycle and lifecycleScope tie coroutine execution to Android component lifecycle states, avoiding wasted work and crashes from stale UI updates.