๐ŸŸฃ Kotlin Programming

Flow vs StateFlow vs SharedFlow

Flow, StateFlow and SharedFlow look similar and are often used interchangeably, which causes lost events, duplicate network calls and screens that don't update. Here's the precise difference and a simple rule for choosing.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 4 min read ๐ŸŽฏ Intermediate
On this page
  1. The one-table summary
  2. Flow: the cold default for data sources
  3. StateFlow: state holders for the UI
  4. SharedFlow: broadcasts
  5. One-off UI events: which tool?
  6. Decision rule
  7. Common mistakes
  8. Frequently asked questions
  9. Conclusion

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

PropertyFlow (cold)StateFlow (hot)SharedFlow (hot)
Runs whenEach collector starts itIndependent of collectorsIndependent of collectors
Multiple collectorsEach gets its own executionShare one value streamShare one stream
Current valueNoAlways (.value)No (optional replay cache)
Initial valueNoRequiredNot required
New collector receivesEverything from the startThe latest value immediatelyreplay most recent values (default 0)
Duplicate consecutive valuesEmittedSkipped (equality-based)Emitted
Best forData streams from repositories (Room, network)UI stateBroadcast events to several listeners
Cold flow versus StateFlow with two collectors Top: a cold flow with two collectors runs its producer twice, causing two network calls. Bottom: a StateFlow created with stateIn runs the producer once and both collectors receive the same latest value. Cold Flow: a producer per collector flow { api.load() } #1 flow { api.load() } #2 collector A collector B 2 network calls StateFlow via stateIn: one shared producer flow { api.load() } ร—1 StateFlow.value = latest collector A collector B 1 call, shared
Cold flows are recipes that run per collector. Hot flows are broadcasts that run once and are shared.

Flow: the cold default for data sources

Repositories expose cold FlowsKotlin
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

Two ways to produce StateFlow in a ViewModelKotlin
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 }
}
StateFlow uses equality

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

App-wide event bus for sign-outKotlin
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.
Event as state (recommended)Kotlin
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

  1. Exposing data from a repository or data source? Return a cold Flow.
  2. Exposing state to the UI? Use a StateFlow (via stateIn or MutableStateFlow).
  3. Broadcasting to several independent listeners, where missed values are acceptable or handled by replay? Use a SharedFlow.
  4. One-off UI event? Prefer state. Otherwise use a buffered Channel collected with repeatOnLifecycle.

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 stateIn inside 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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp