Introduction

Kotlin StateFlow is a hot flow that always replays its current value to new collectors. However, StateFlow uses equality comparison to determine whether to emit a new value — if you assign the same value (by equals() comparison), downstream collectors receive nothing. Additionally, when collecting StateFlow in Android UI with lifecycle-aware APIs, emissions during the STOPPED state are missed. Confusion between StateFlow (always has a value, replays to new collectors) and SharedFlow (configurable replay, may have no value) leads to missing data in collectors.

Symptoms

  • Collector does not receive the initial value
  • Updating StateFlow with same value does not trigger recomposition
  • UI does not update after configuration change (rotation)
  • collectAsState shows stale data
  • StateFlow emission lost when app is in background
  • Multiple collectors receive different values

Common Causes

  • New value equals current value (StateFlow deduplicates by equality)
  • Collector started after emission and StateFlow was updated before collection
  • Using SharedFlow instead of StateFlow (no replay by default)
  • Lifecycle-aware collection skips emissions when lifecycle is not STARTED
  • MutableStateFlow updated from multiple threads without proper synchronization

Step-by-Step Fix

  1. 1.Understand StateFlow value deduplication:
  2. 2.```kotlin
  3. 3.// StateFlow deduplicates by equals() comparison
  4. 4.val _state = MutableStateFlow(UserData("John", 30))

// This does NOT emit - value is the same _state.value = UserData("John", 30) // Collectors receive nothing

// This DOES emit - value is different _state.value = UserData("John", 31) // Collectors receive the new value

// If you need to emit the same value, use a wrapper data class Event<out T>(val content: T, val id: UUID = UUID.randomUUID())

val _state = MutableStateFlow(Event(UserData("John", 30)))

// Each Event is unique because of the UUID _state.value = Event(UserData("John", 30)) // Collectors receive it because the Event instances are different

// OR use SharedFlow for one-time events private val _events = MutableSharedFlow<UiEvent>(extraBufferCapacity = 1) val events: SharedFlow<UiEvent> = _events

fun showToast(message: String) { _events.tryEmit(UiEvent.ShowToast(message)) } ```

  1. 1.Use SharedFlow when StateFlow semantics do not fit:
  2. 2.```kotlin
  3. 3.// StateFlow: always has a value, deduplicates, replays to new collectors
  4. 4.val stateFlow = MutableStateFlow(0) // Initial value REQUIRED

// SharedFlow: no initial value, configurable replay, no deduplication val sharedFlow = MutableSharedFlow<Int>( replay = 0, // New collectors get nothing until new emission extraBufferCapacity = 10 // Buffer 10 emissions )

// SharedFlow with replay = 1 (like StateFlow but no deduplication) val replayFlow = MutableSharedFlow<Int>( replay = 1, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST )

// Emit the same value multiple times replayFlow.tryEmit(42) replayFlow.tryEmit(42) // Both emissions are delivered (no deduplication) replayFlow.tryEmit(42) // All three are delivered

// Use SharedFlow for one-time events (navigation, toasts, snackbar) class NavigationEvent { private val _navigation = MutableSharedFlow<NavEvent>(extraBufferCapacity = 1) val navigation: SharedFlow<NavEvent> = _navigation

fun navigateTo(destination: String) { _navigation.tryEmit(NavEvent.Navigate(destination)) } } ```

  1. 1.**Collect StateFlow with lifecycle awareness in Android":
  2. 2.```kotlin
  3. 3.// WRONG - collectAsState collects even when lifecycle is DESTROYED
  4. 4.@Composable
  5. 5.fun UserScreen(viewModel: UserViewModel) {
  6. 6.val state by viewModel.state.collectAsState() // May collect in wrong lifecycle
  7. 7.UserContent(state)
  8. 8.}

// CORRECT - lifecycle-aware collection @Composable fun UserScreen(viewModel: UserViewModel, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current) { val state by viewModel.state.collectAsStateWithLifecycle( lifecycle = lifecycleOwner.lifecycle, minActiveState = Lifecycle.State.STARTED ) UserContent(state) }

// For non-Compose Android - lifecycle-aware collection lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { state -> updateUI(state) // Only collects when lifecycle is STARTED+ } } }

// If you need the value immediately regardless of lifecycle lifecycleScope.launch { // Get current value immediately updateUI(viewModel.state.value)

// Then collect changes viewModel.state.collect { state -> updateUI(state) } } ```

Prevention

  • Use StateFlow for state that always has a current value (UI state, config)
  • Use SharedFlow for one-time events (navigation, toasts, dialogs)
  • Remember that StateFlow deduplicates by equals() — wrap in a unique container if needed
  • Use collectAsStateWithLifecycle in Compose for lifecycle-aware collection
  • Always set an initial value for MutableStateFlow in the property declaration
  • Test flow emissions with Turbine library for reliable flow testing