Introduction

SharedFlow in Kotlin is a hot flow that broadcasts emissions to multiple collectors. Unlike StateFlow, it does not have a default replay value. If a collector starts collecting after an emission has already occurred, it misses that emission entirely. This is a common source of bugs in Android where UI events are emitted before the Activity/Fragment begins collecting.

Symptoms

  • UI does not update after ViewModel emits a value
  • collect block is never entered despite emissions
  • Works with StateFlow but not with SharedFlow
  • Event is emitted in init block but collector starts in onCreate
  • One-shot events (navigation, snackbar) are lost on configuration change

Example problem: ```kotlin class MyViewModel : ViewModel() { private val _navigationEvent = MutableSharedFlow<Screen>() val navigationEvent: SharedFlow<Screen> = _navigationEvent

init { // Emitted immediately viewModelScope.launch { _navigationEvent.emit(Screen.Home) } } }

class MyActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

// Collector starts AFTER the emission - misses it! lifecycleScope.launch { viewModel.navigationEvent.collect { screen -> navigate(screen) // Never called } } } } ```

Common Causes

  • SharedFlow created with default replay = 0
  • Emission happens before collect is called
  • Collector is cancelled and restarted (configuration change)
  • Using SharedFlow for state instead of StateFlow
  • Multiple collectors but only some receive emissions

Step-by-Step Fix

  1. 1.Use StateFlow for state, SharedFlow for events:
  2. 2.```kotlin
  3. 3.// For state that should always have a current value:
  4. 4.private val _uiState = MutableStateFlow(UiState.Loading)
  5. 5.val uiState: StateFlow<UiState> = _uiState

// For one-shot events: private val _events = MutableSharedFlow<UiEvent>(extraBufferCapacity = 1) val events: SharedFlow<UiEvent> = _events ```

  1. 1.Add replay to SharedFlow for late subscribers:
  2. 2.```kotlin
  3. 3.// Replay the last emission to new collectors
  4. 4.private val _navigationEvent = MutableSharedFlow<Screen>(replay = 1)

// Now collectors that start after the emission will receive the last value lifecycleScope.launch { viewModel.navigationEvent.collect { screen -> navigate(screen) // Will receive the last emitted value } } ```

  1. 1.Use a Channel for one-shot events:
  2. 2.```kotlin
  3. 3.class MyViewModel : ViewModel() {
  4. 4.private val _navigationChannel = Channel<Screen>(Channel.CONFLATED)
  5. 5.val navigationEvent = _navigationChannel.receiveAsFlow()

fun onButtonClicked() { viewModelScope.launch { _navigationChannel.send(Screen.Home) } } }

class MyActivity : AppCompatActivity() { override fun onResume() { super.onResume() // Collect only when activity is visible lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.navigationEvent.collect { screen -> navigate(screen) } } } } } ```

  1. 1.Buffer emissions for slow collectors:
  2. 2.```kotlin
  3. 3.// Buffer up to 10 events if collector is slow
  4. 4.private val _events = MutableSharedFlow<UiEvent>(
  5. 5.replay = 0,
  6. 6.extraBufferCapacity = 10,
  7. 7.onBufferOverflow = BufferOverflow.DROP_OLDEST
  8. 8.)

// This prevents the emitter from suspending when collector is slow ```

Prevention

  • Use StateFlow for UI state (always has a value, replay = 1)
  • Use SharedFlow with replay = 1 for values that late subscribers should see
  • Use Channel or SharedFlow(extraBufferCapacity = 1) for one-shot events
  • Start collectors early: in onCreate before any emissions
  • Use repeatOnLifecycle to collect only when the UI is visible
  • Test timing-dependent flows with runTest and TestDispatcher