StateFlow vs SharedFlow vs LiveData vs Channels
The hot stream comparison every senior Android developer must know
Open interactive version (quiz + challenge)Real-world analogy
What is it?
StateFlow, SharedFlow, LiveData, and Channel are all tools for observable state and event streaming in Android. StateFlow is the current industry standard for UI state. SharedFlow handles broadcast events. LiveData is legacy but still common in older codebases. Channels are for point-to-point messaging. Knowing when to use each and being able to migrate between them is a core senior Android interview topic.
Real-world relevance
In a real-time SaaS collaboration app: DocumentViewModel exposes val uiState: StateFlow for screen state. val navigationEvents: SharedFlow handles one-shot navigation with replay=0. The room database query uses stateIn(WhileSubscribed(5000)) to avoid restarting on rotation. Legacy screens still using LiveData are migrated using asFlow() and asLiveData() bridge extensions.
Key points
- StateFlow — hot, always has value — StateFlow(initialValue) is a hot observable that always holds a current value. New collectors receive the current value immediately on subscription. Replay = 1 (always). Requires an initial value. Conflates — if no one is collecting, intermediate values are dropped and only the latest is kept.
- MutableStateFlow vs StateFlow — _uiState = MutableStateFlow(Loading) — mutable, write from ViewModel. val uiState: StateFlow = _uiState.asStateFlow() — read-only, exposed to UI. This is the standard ViewModel backing property pattern. The underscore prefix convention signals the mutable version is private.
- SharedFlow — hot, configurable — MutableSharedFlow(replay = 0, extraBufferCapacity = 64) is a hot flow with configurable replay and buffer. replay = 0 means new collectors miss past events. replay = 1 behaves like StateFlow without requiring an initial value. Use for events, not state.
- StateFlow vs SharedFlow key differences — StateFlow: requires initial value, always has current value, conflates (drops intermediate), equality-based emission (won't re-emit same value). SharedFlow: no initial value required, configurable replay, configurable buffer, always emits every value even if unchanged.
- LiveData — lifecycle-aware, legacy — LiveData is lifecycle-aware — only delivers updates when the observer is in STARTED/RESUMED state. Tied to Android lifecycle. No initial value required (starts null). Cannot use suspend functions natively. In 2025+, LiveData is considered legacy — prefer StateFlow + repeatOnLifecycle.
- Why StateFlow replaced LiveData — StateFlow: Kotlin-first, works with Flow operators, testable without Android framework, works in non-UI layers, requires explicit lifecycle handling (repeatOnLifecycle). LiveData: Java-compatible, auto lifecycle-aware but requires Android context everywhere, harder to test, no operator support.
- Channel — point-to-point, once delivery — Channel is a non-broadcast queue — each value is consumed by EXACTLY one receiver. send()/receive() are suspend functions. Multiple collectors cannot all receive the same value. Use for producer-consumer patterns, not UI state. Channel is the foundation of channelFlow and actor patterns.
- One-shot events: the classic problem — Navigation events, snackbar messages, and dialogs are one-time — they must not re-trigger on screen rotation. StateFlow conflates and re-emits current value on collection restart. Solutions: SharedFlow(replay=0), Channel, or UiEvent sealed class in StateFlow that gets cleared after consumption.
- Migrating LiveData to StateFlow — Replace LiveData with StateFlow. Replace postValue/setValue with _flow.value = or _flow.update { }. Replace observe() with repeatOnLifecycle + collect. Replace observeAsState() in Compose with collectAsStateWithLifecycle() (Lifecycle-Compose library).
- stateIn() and shareIn() operators — flow.stateIn(scope, SharingStarted, initialValue) converts cold Flow to StateFlow. flow.shareIn(scope, SharingStarted) converts to SharedFlow. SharingStarted.WhileSubscribed(5000) stops the upstream after 5s with no subscribers — saves resources during config change.
- update {} function on MutableStateFlow — _state.update { currentState -> currentState.copy(isLoading = false) } — atomically updates StateFlow using the current value. Thread-safe; avoids race conditions vs _state.value = _state.value.copy(...) which can lose updates under concurrency.
- Backpressure handling — If producers emit faster than consumers collect: StateFlow silently drops intermediate values (conflation). SharedFlow with buffer: stores items in buffer up to extraBufferCapacity, then suspends or drops based on onBufferOverflow strategy. Channel: suspends sender by default (SUSPEND strategy).
Code example
// Standard ViewModel pattern — StateFlow for UI state
class DocumentViewModel(
private val repo: DocumentRepository
) : ViewModel() {
// Private mutable, public read-only — standard pattern
private val _uiState = MutableStateFlow<DocumentUiState>(DocumentUiState.Loading)
val uiState: StateFlow<DocumentUiState> = _uiState.asStateFlow()
// One-shot events — SharedFlow with replay=0
private val _events = MutableSharedFlow<DocumentEvent>(
replay = 0,
extraBufferCapacity = 16,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<DocumentEvent> = _events.asSharedFlow()
// StateFlow backed by Room Flow — stops upstream 5s after no subscribers
val documents: StateFlow<List<Document>> =
repo.getAllDocuments()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
// Thread-safe state update
fun onDocumentSaved(doc: Document) {
_uiState.update { currentState ->
when (currentState) {
is DocumentUiState.Success -> currentState.copy(
documents = currentState.documents + doc,
lastSaved = System.currentTimeMillis()
)
else -> currentState
}
}
}
// Emit one-shot navigation event
fun onShareClicked(docId: String) {
viewModelScope.launch {
_events.emit(DocumentEvent.NavigateToShare(docId))
}
}
}
// UI layer — collect with repeatOnLifecycle
class DocumentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.uiState.collect { renderState(it) } }
launch { viewModel.events.collect { handleEvent(it) } }
}
}
}
}
// Compose — collectAsStateWithLifecycle (preferred over collectAsState)
@Composable
fun DocumentScreen(viewModel: DocumentViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// ...
}Line-by-line walkthrough
- 1. private val _uiState = MutableStateFlow(Loading) — underscore prefix signals mutable/private; class maintains single source of truth
- 2. val uiState = _uiState.asStateFlow() — read-only wrapper; UI can only read, not write state
- 3. MutableSharedFlow(replay=0, extraBufferCapacity=16) — replay=0: new collectors miss past events; buffer: prevents suspension on slow collectors
- 4. BufferOverflow.DROP_OLDEST — if buffer fills (16 events), oldest undelivered event is dropped; prevents backpressure blocking
- 5. repo.getAllDocuments().stateIn(viewModelScope, WhileSubscribed(5_000), emptyList()) — Room flow → hot StateFlow; stops 5s after no UI
- 6. _uiState.update { currentState -> } — atomic read-modify-write; safe under concurrent updates from multiple coroutines
- 7. currentState.copy(documents = ... + doc) — data class copy for immutable state update; original state unchanged
- 8. _events.emit(NavigateToShare(docId)) — must be in coroutine; suspends if buffer full (with SUSPEND strategy)
- 9. repeatOnLifecycle(STARTED) { launch{ uiState.collect } launch{ events.collect } } — both collections restart on STARTED, stop on STOPPED
Spot the bug
class NotificationViewModel : ViewModel() {
val notifications = MutableStateFlow<List<Notification>>(emptyList())
// For showing one-time snackbar messages
val snackbarMessage = MutableStateFlow<String?>(null)
fun showMessage(msg: String) {
snackbarMessage.value = msg
}
}
// In Fragment:
lifecycleScope.launch {
viewModel.snackbarMessage.collect { message ->
if (message != null) showSnackbar(message)
// After showing, never resets!
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- StateFlow and SharedFlow (developer.android.com)
- Migrating from LiveData to StateFlow (developer.android.com)
- StateFlow API Reference (kotlinlang.org)
- collectAsStateWithLifecycle (developer.android.com)