Lesson 10 of 83 advanced

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

StateFlow is a scoreboard — it always shows the CURRENT score, new fans who arrive can immediately see it, and it always has a value. SharedFlow is a radio broadcast — you get only what's broadcast after you tune in. LiveData is a bulletin board only readable inside the building (lifecycle-aware). Channel is a one-way pipe — one sender, one receiver, one delivery.

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

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. 1. private val _uiState = MutableStateFlow(Loading) — underscore prefix signals mutable/private; class maintains single source of truth
  2. 2. val uiState = _uiState.asStateFlow() — read-only wrapper; UI can only read, not write state
  3. 3. MutableSharedFlow(replay=0, extraBufferCapacity=16) — replay=0: new collectors miss past events; buffer: prevents suspension on slow collectors
  4. 4. BufferOverflow.DROP_OLDEST — if buffer fills (16 events), oldest undelivered event is dropped; prevents backpressure blocking
  5. 5. repo.getAllDocuments().stateIn(viewModelScope, WhileSubscribed(5_000), emptyList()) — Room flow → hot StateFlow; stops 5s after no UI
  6. 6. _uiState.update { currentState -> } — atomic read-modify-write; safe under concurrent updates from multiple coroutines
  7. 7. currentState.copy(documents = ... + doc) — data class copy for immutable state update; original state unchanged
  8. 8. _events.emit(NavigateToShare(docId)) — must be in coroutine; suspends if buffer full (with SUSPEND strategy)
  9. 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?
Using StateFlow for one-time events causes re-showing the snackbar on rotation. StateFlow replays current value on new subscription.
Show answer
Bug: StateFlow(null) replays the current value to every new collector — on screen rotation, the snackbar will show again because the state still holds the message. Fix option 1: Use SharedFlow(replay=0) for events — new collectors miss past events. Fix option 2: After showing the snackbar, reset the state: viewModel.clearMessage() which sets snackbarMessage.value = null. Fix option 3 (best): Use a sealed class UiEvent that wraps one-time events, model them in UiState, and clear them after consumption using update { it.copy(pendingEvent = null) }.

Explain like I'm 5

StateFlow is like a parking meter display — it always shows the CURRENT time remaining, and anyone who looks at it right now sees the current value. SharedFlow is like a radio station — you only hear songs that play AFTER you tune in. LiveData is like a school PA system that only broadcasts to classrooms when the teacher is actually in the room. Channel is like passing a note to exactly one person — they get it, no one else does.

Fun fact

The debate between Channel and SharedFlow for one-shot events (like navigation) is one of the most discussed topics in the Android community. Google's official stance (2022+) is to use SharedFlow(replay=0) for events, or model events as part of UiState and clear them after consumption — never use Channel for UI events because it has no lifecycle awareness.

Hands-on challenge

Design the complete reactive state layer for a document collaboration ViewModel. Include: 1) StateFlow for screen state using the private/public backing property pattern. 2) SharedFlow for navigation/snackbar events with replay=0 and a buffer of 32. 3) val collaborators: StateFlow> backed by a Room Flow using stateIn with WhileSubscribed(5000). 4) A thread-safe addCollaborator() function using update{}. 5) An onError() that emits both a UiState update AND a one-time snackbar event. Show the Fragment/Composable collection code.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery