On this page
Two collectors of the same Flow trigger two network requests. A snackbar event fires while the screen is rotating and disappears. A StateFlow doesn't emit after you "update" a list. Each of these comes from choosing the wrong kind of flow, or misunderstanding how it behaves.
The one-table summary
| Property | Flow (cold) | StateFlow (hot) | SharedFlow (hot) |
|---|---|---|---|
| Runs when | Each collector starts it | Independent of collectors | Independent of collectors |
| Multiple collectors | Each gets its own execution | Share one value stream | Share one stream |
| Current value | No | Always (.value) | No (optional replay cache) |
| Initial value | No | Required | Not required |
| New collector receives | Everything from the start | The latest value immediately | replay most recent values (default 0) |
| Duplicate consecutive values | Emitted | Skipped (equality-based) | Emitted |
| Best for | Data streams from repositories (Room, network) | UI state | Broadcast events to several listeners |
Flow: the cold default for data sources
class NotesRepository(private val dao: NoteDao) {
fun observeNotes(): Flow<List<Note>> = dao.observeAll().map { rows -> rows.map { it.toDomain() } }
}A cold flow does nothing until collected, and each collector gets its own run. That's ideal for repositories: the caller decides when and how long to observe.
StateFlow: state holders for the UI
class NotesViewModel(repo: NotesRepository) : ViewModel() {
// 1. Derive state from a cold Flow with stateIn: shared, cached, lifecycle-friendly
val notes: StateFlow<List<Note>> = repo.observeNotes()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initialValue = emptyList())
// 2. Hold mutable state directly with MutableStateFlow
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
fun onQueryChange(q: String) { _query.value = q }
}If you mutate a list inside the current value and set it again, StateFlow sees an equal object and emits nothing. Always emit a new immutable value, for example _state.update { it.copy(items = it.items + item) }.
SharingStarted.WhileSubscribed(5_000) stops the upstream five seconds after the last collector leaves. That survives rotation without restarting, but stops work when the user truly leaves the screen.
SharedFlow: broadcasts
class SessionEvents {
private val _signedOut = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val signedOut: SharedFlow<Unit> = _signedOut.asSharedFlow()
fun notifySignedOut() { _signedOut.tryEmit(Unit) }
}Key configuration: replay (how many past values new collectors receive), extraBufferCapacity and onBufferOverflow. With the default replay = 0, events emitted while nobody is collecting are lost. That's correct for some broadcasts, and wrong for one-off UI events.
One-off UI events: which tool?
Navigation and snackbars are where these choices bite. The options:
- Model the event as state (recommended by Android's architecture guidance): store
userMessage: String?in UI state and clear it after showing. Nothing can be lost. Channel+receiveAsFlow(): buffers events until a single collector receives them. Each event is delivered to one collector.- SharedFlow with replay 0: events emitted during rotation, when nobody is collecting, are dropped. Generally avoid it for UI events.
data class EditorUiState(val text: String = "", val userMessage: String? = null)
fun save() = viewModelScope.launch {
repo.save(_state.value.text)
_state.update { it.copy(userMessage = "Saved") }
}
fun messageShown() = _state.update { it.copy(userMessage = null) }
// In Compose:
// state.userMessage?.let { msg ->
// LaunchedEffect(msg) { snackbarHostState.showSnackbar(msg); onMessageShown() }
// }Decision rule
- Exposing data from a repository or data source? Return a cold
Flow. - Exposing state to the UI? Use a
StateFlow(viastateInorMutableStateFlow). - Broadcasting to several independent listeners, where missed values are acceptable or handled by replay? Use a
SharedFlow. - One-off UI event? Prefer state. Otherwise use a buffered
Channelcollected withrepeatOnLifecycle.
Common mistakes
- Exposing a cold flow from a ViewModel and collecting it from several composables, which restarts work on each collection and on every rotation.
- Using
stateIn(..., SharingStarted.Eagerly, ...)everywhere, which keeps upstreams running even when no UI is visible. - Calling
stateIninside a function that's called repeatedly, creating a new StateFlow each time. Create it once, as a property. - Using SharedFlow with no replay for navigation events.
Frequently asked questions
Is StateFlow a replacement for LiveData?
For new Kotlin code, generally yes. Collect it with collectAsStateWithLifecycle() or repeatOnLifecycle to get LiveData's lifecycle awareness.
Is StateFlow just a SharedFlow with replay 1?
Close: it behaves like a SharedFlow with replay 1, a required initial value, conflation and equality-based de-duplication. It also exposes a synchronous .value.
How do I test these?
Use runTest and collect into a list in backgroundScope, or use a library such as Turbine. See Testing ViewModels with Coroutines and Flow.
Conclusion
Flow is a cold recipe for data. StateFlow is hot, always holds the latest value, and is the right tool for UI state. SharedFlow is a configurable broadcast. Expose cold flows from repositories, convert them to StateFlow in ViewModels with stateIn, and model one-off UI events as state whenever you can.
Next step: master the building block underneath all of these in Suspend Functions Explained.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Kotlin fluency makes everything else in Android easier, from coroutines and Flow to Compose and clean architecture. The fastest way to build that fluency is to write real Kotlin every day and get feedback on it.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.