Hot flows are one of the most asked topics in Android interviews. This covers SharedFlow, StateFlow, converting cold flows to hot, lifecycle-aware collection, and how all of it fits together in a real Android app.
A cold flow doesn’t start emitting until a collector subscribes. Each collector gets its own independent execution — two collectors means the producer runs twice. A hot flow is active regardless of collectors. It emits whether there are zero or ten collectors, and multiple collectors share the same stream. flow {} creates a cold flow. SharedFlow and StateFlow are hot.
StateFlow always has a current value, requires an initial value, and uses distinctUntilChanged — it won’t emit the same value twice in a row. It has a replay of 1 and built-in conflation. Designed for representing UI state.
SharedFlow is more configurable. It supports custom replay, buffer size, and overflow handling. It doesn’t require an initial value and doesn’t filter duplicate emissions.
Key differences:
.value)MutableSharedFlow exposes emit() and tryEmit() for sending values. MutableStateFlow exposes a value property for reading and writing state. Keep the mutable version private and expose the read-only type to the UI.
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
private val _events = MutableSharedFlow<SearchEvent>()
val events: SharedFlow<SearchEvent> = _events.asSharedFlow()
fun onQueryChanged(text: String) {
_query.value = text
}
}
Use StateFlow for state — UI state, loading status, selected item. Use SharedFlow for events that should be processed once — navigation commands, snackbar messages, error toasts.
StateFlow conflates values, so if you emit two navigation events quickly, the second might be lost. SharedFlow with replay = 0 and extraBufferCapacity = 1 handles one-shot events better.
replay determines how many previously emitted values a new collector receives when it starts. With replay = 0, new collectors only get values emitted after subscribing. With replay = 1, they immediately get the most recent value. StateFlow is a SharedFlow with replay = 1 and distinctUntilChanged.
callbackFlow converts a multi-shot callback API into a cold Flow. It creates a channel internally and lets you send values from callbacks using trySend(). The awaitClose block is mandatory for cleanup.
fun locationUpdates(client: FusedLocationProviderClient): Flow<Location> =
callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { trySend(it) }
}
}
client.requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
awaitClose { client.removeLocationUpdates(callback) }
}
channelFlow runs in a ProducerScope where you can launch multiple coroutines that send values concurrently. callbackFlow is for wrapping callback-based APIs. Both use channels under the hood, but callbackFlow enforces awaitClose for resource cleanup.
fun mergedResults(query: String): Flow<SearchResult> = channelFlow {
launch { send(localDb.search(query)) }
launch { send(remoteApi.search(query)) }
}
When converting cold to hot with stateIn or shareIn:
WhileSubscribed(5_000) is the standard for ViewModels — survives configuration changes but stops when the user navigates away.val uiState: StateFlow<HomeUiState> = repository.observeItems()
.map { items -> HomeUiState(items = items) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = HomeUiState.Loading
)
stateIn converts a cold flow to StateFlow. Requires an initial value, gives distinctUntilChanged and .value access. shareIn converts to SharedFlow. No initial value required, supports configurable replay.
Use stateIn when downstream needs the current value at any time (UI state). Use shareIn for broadcasting events or replay greater than 1.
Use repeatOnLifecycle. It starts collection when the lifecycle reaches the specified state and cancels when it drops below.
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}
}
}
For multiple flows, launch separate coroutines inside repeatOnLifecycle.
flowWithLifecycle is a Flow operator that emits values only when the lifecycle is at least in the specified state. Use it for a single flow — it reads cleaner as a chain. For multiple flows, repeatOnLifecycle with separate launch blocks is better.
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { state -> updateUi(state) }
}
collectAsStateWithLifecycle is the Compose equivalent of repeatOnLifecycle. It collects a flow into Compose State while respecting the lifecycle. Collection stops when the lifecycle drops below the minimum active state.
@Composable
fun HomeScreen(viewModel: HomeViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is HomeUiState.Loading -> LoadingIndicator()
is HomeUiState.Success -> ItemList(uiState.items)
}
}
This is the recommended way to collect flows in Compose — don’t use collectAsState() because it doesn’t stop collection in the background.
SharedFlow handles backpressure through its buffer configuration. The total buffer is replay + extraBufferCapacity. When full:
emit() suspends until space is available.tryEmit() is the non-suspending alternative. It returns false if the buffer is full and overflow is SUSPEND. With DROP_OLDEST or DROP_LATEST, tryEmit() always succeeds.
During a configuration change, the Activity is destroyed and recreated. The ViewModel survives, but the UI collector is cancelled. The 5-second timeout means WhileSubscribed(5_000) waits before stopping upstream collection. A configuration change takes under 1-2 seconds, so the new Activity resubscribes before the timeout. The upstream stays active and the new collector gets the current state immediately. With WhileSubscribed(0), the upstream would stop and restart on every rotation.
Use SharedFlow with replay = 0 and extraBufferCapacity = 1. Each event is delivered without replaying on resubscription. StateFlow would replay the last event on configuration changes.
class CartViewModel : ViewModel() {
private val _events = MutableSharedFlow<CartEvent>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<CartEvent> = _events.asSharedFlow()
fun onCheckout() {
viewModelScope.launch {
val result = repository.checkout()
if (result.isSuccess) {
_events.emit(CartEvent.NavigateToConfirmation)
}
}
}
}
Both handle slow collectors differently. conflate() drops intermediate values — the collector always gets the latest available when it finishes processing. collectLatest {} cancels the previous collector’s work when a new value arrives and restarts.
Use conflate() when processing is non-cancellable (database writes). Use collectLatest when processing is cancellable and you only care about the most recent result (search triggering a network call).
stateIn starts a coroutine in the provided scope that collects the upstream cold flow and emits values to the StateFlow. The cold flow runs once in this shared coroutine, regardless of how many UI collectors subscribe. Without stateIn, three collectors would trigger three separate executions of the cold flow.
subscriptionCount is a StateFlow<Int> that tracks active collectors. SharingStarted.WhileSubscribed() uses it internally to decide when to start and stop collection. You can use it for custom logic like pausing a sensor stream when no one is subscribed.
Yes. SharingStarted is an interface with a command(subscriptionCount: StateFlow<Int>): Flow<SharingCommand> function. You return START and STOP commands based on subscription count. The built-in strategies are implementations of this interface.
emit() on a MutableSharedFlow with no collectors and replay = 0?repeatOnLifecycle block?collectAsState() and collectAsStateWithLifecycle()?combine() work with StateFlows in a ViewModel?stateIn with WhileSubscribed?launchIn and collect?