androidengineers.Book a session

Modern Android Architecture Components

LiveData vs StateFlow/SharedFlow

article25 minMedium

LiveData: The Original Lifecycle-Aware Observable

LiveData was introduced in 2017 to solve Activity/Fragment lifecycle management. It automatically stops delivering updates when the observer is stopped, preventing crashes from updating destroyed UI.

// LiveData in ViewModel
class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user

    private val _isLoading = MutableLiveData<Boolean>(false)
    val isLoading: LiveData<Boolean> = _isLoading

    fun loadUser(id: String) {
        _isLoading.value = true
        viewModelScope.launch {
            val result = userRepository.getUser(id)
            _user.value = result.getOrNull()
            _isLoading.value = false
        }
    }
}

// Activity/Fragment observes
viewModel.user.observe(viewLifecycleOwner) { user ->
    binding.tvName.text = user.name
}

LiveData Limitations

  1. Not idiomatic Kotlin — predates coroutines and Kotlin Flow
  2. No backpressure — value just gets replaced; can't buffer
  3. Always on the main thread — can't emit from background easily
  4. No operator supportTransformations.map/switchMap is clunky vs Flow operators
  5. Event handling is broken by default — late subscribers get the last value (sticky behavior)
  6. Java-based — heavier than StateFlow for Kotlin codebases

StateFlow: LiveData's Kotlin Successor

StateFlow is a Flow subtype that always holds a current value and replays it to new collectors. It's the Kotlin-native replacement for LiveData in ViewModels.

class UserViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun loadUser(id: String) {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            userRepository.getUser(id)
                .onSuccess { user ->
                    _uiState.update { it.copy(isLoading = false, user = user) }
                }
                .onFailure { error ->
                    _uiState.update { it.copy(isLoading = false, error = error.message) }
                }
        }
    }
}

StateFlow Key Behaviors

Equality deduplication: StateFlow uses equals() to compare new values. If the new value equals the current value, no emission occurs. This prevents redundant re-renders but can surprise you with reference types.

// This will NOT emit a second time — same value
_uiState.value = UserUiState(isLoading = true) // emits
_uiState.value = UserUiState(isLoading = true) // NOT emitted — equal!

// This WILL always emit — mutable list reference changes
_listState.value = mutableListOf("A", "B")
_listState.value = mutableListOf("A", "B") // emits — different reference

Must use repeatOnLifecycle: Unlike LiveData, Flow does not automatically stop collection when UI is backgrounded. Collecting in lifecycleScope.launch {} alone will continue in the background, wasting resources and potentially crashing.

// WRONG — continues collecting when app is in background
lifecycleScope.launch {
    viewModel.uiState.collect { render(it) }
}

// CORRECT — stops when STARTED, resumes when STARTED again
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { render(it) }
    }
}

SharedFlow: For Events and Broadcasts

SharedFlow is a hot flow with no initial value. It's ideal for one-shot events (navigation, Snackbars) and broadcasting to multiple collectors.

class EventViewModel : ViewModel() {

    // replay = 0: no replay for late collectors (good for events)
    // extraBufferCapacity = 1: don't drop events if collector is slow
    private val _events = MutableSharedFlow<UiEvent>(
        replay = 0,
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val events: SharedFlow<UiEvent> = _events.asSharedFlow()

    fun onLoginSuccess() {
        viewModelScope.launch {
            _events.emit(UiEvent.NavigateToHome)
        }
    }
}

sealed interface UiEvent {
    object NavigateToHome : UiEvent
    data class ShowError(val message: String) : UiEvent
}

Channel vs SharedFlow for Events

// Channel approach (simpler, recommended for single consumer)
private val _events = Channel<UiEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()

// Emit:
viewModelScope.launch { _events.send(UiEvent.NavigateToHome) }

// SharedFlow approach (for multiple consumers or complex buffering)
private val _events = MutableSharedFlow<UiEvent>(extraBufferCapacity = 1)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()

For most Android apps, Channel is simpler and covers 90% of event use cases.


The Event Anti-Pattern with StateFlow

A common mistake: storing one-shot events in StateFlow. New collectors (e.g., after rotation) will receive the last event and trigger it again.

// WRONG — event in StateFlow gets re-triggered on rotation
data class UiState(
    val navigateToDetail: Boolean = false,  // anti-pattern!
    val showError: String? = null
)

// Fragment collects uiState; on rotation:
// 1. New Fragment instance is created
// 2. New collector subscribes to StateFlow
// 3. StateFlow replays last value: navigateToDetail = true
// 4. Fragment navigates again — BUG!

// CORRECT — events in Channel, state in StateFlow
class CorrectViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    private val _events = Channel<UiEvent>(Channel.BUFFERED)
    val events = _events.receiveAsFlow()
}

The stateIn() Operator

stateIn() converts a cold Flow into a StateFlow. This is useful for converting repository Flows into ViewModel state.

@HiltViewModel
class UserListViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    // Converts the repository Flow to StateFlow, handling loading/error states
    val users: StateFlow<UiState<List<User>>> =
        userRepository.observeUsers()
            .map<List<User>, UiState<List<User>>> { UiState.Success(it) }
            .catch { emit(UiState.Error(it.message ?: "Error")) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000), // stays alive 5s after last subscriber
                initialValue = UiState.Loading
            )
}

SharingStarted.WhileSubscribed(5_000) is the recommended default — it keeps the upstream Flow alive for 5 seconds after the last subscriber disappears, covering rotation without restarting network requests.


Migration Guide: LiveData → StateFlow

// Before: LiveData
private val _name = MutableLiveData<String>()
val name: LiveData<String> = _name

// After: StateFlow
private val _name = MutableStateFlow("")
val name: StateFlow<String> = _name.asStateFlow()

// Before: observe in Fragment
viewModel.name.observe(viewLifecycleOwner) { binding.tvName.text = it }

// After: collect in Fragment
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.name.collect { binding.tvName.text = it }
    }
}

// Before: Transformations
val upperName: LiveData<String> = Transformations.map(_name) { it.uppercase() }

// After: Flow operators
val upperName: StateFlow<String> = _name
    .map { it.uppercase() }
    .stateIn(viewModelScope, SharingStarted.Eagerly, "")

Feature Comparison

FeatureLiveDataStateFlowSharedFlow
Current value holderYesYesNo (unless replay > 0)
Lifecycle awarenessBuilt-inManual (repeatOnLifecycle)Manual
Equality dedupNoYesNo
Kotlin operatorsLimitedFull Flow operatorsFull Flow operators
ThreadingMain threadAny threadAny thread
BackpressureNoLatest-winsConfigurable
Multiple collectorsYesYesYes
Event deliverySticky (bad for events)Sticky (bad for events)Configurable (good for events)
Android dependencyYes (androidx.lifecycle)No (kotlinx.coroutines)No

Key Takeaways

ConceptSummary
LiveDataLifecycle-aware but limited; Java-based; avoid in new code
StateFlowLiveData replacement; requires repeatOnLifecycle; equality dedup
SharedFlowMulti-consumer broadcast; configurable replay and buffer
ChannelSimple one-shot event delivery; receiveAsFlow() for collection
Event anti-patternNever put one-shot events in StateFlow — they re-trigger on rotation
stateIn()Converts repository Flow to StateFlow; use WhileSubscribed(5_000)

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
LiveData vs StateFlow/SharedFlow | Android System Design | Android Engineers