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
collectblock is never entered despite emissions- Works with
StateFlowbut not withSharedFlow - Event is emitted in
initblock but collector starts inonCreate - 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
SharedFlowcreated with defaultreplay = 0- Emission happens before
collectis called - Collector is cancelled and restarted (configuration change)
- Using
SharedFlowfor state instead ofStateFlow - Multiple collectors but only some receive emissions
Step-by-Step Fix
- 1.Use StateFlow for state, SharedFlow for events:
- 2.```kotlin
- 3.// For state that should always have a current value:
- 4.private val _uiState = MutableStateFlow(UiState.Loading)
- 5.val uiState: StateFlow<UiState> = _uiState
// For one-shot events: private val _events = MutableSharedFlow<UiEvent>(extraBufferCapacity = 1) val events: SharedFlow<UiEvent> = _events ```
- 1.Add replay to SharedFlow for late subscribers:
- 2.```kotlin
- 3.// Replay the last emission to new collectors
- 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.Use a Channel for one-shot events:
- 2.```kotlin
- 3.class MyViewModel : ViewModel() {
- 4.private val _navigationChannel = Channel<Screen>(Channel.CONFLATED)
- 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.Buffer emissions for slow collectors:
- 2.```kotlin
- 3.// Buffer up to 10 events if collector is slow
- 4.private val _events = MutableSharedFlow<UiEvent>(
- 5.replay = 0,
- 6.extraBufferCapacity = 10,
- 7.onBufferOverflow = BufferOverflow.DROP_OLDEST
- 8.)
// This prevents the emitter from suspending when collector is slow ```
Prevention
- Use
StateFlowfor UI state (always has a value, replay = 1) - Use
SharedFlowwithreplay = 1for values that late subscribers should see - Use
ChannelorSharedFlow(extraBufferCapacity = 1)for one-shot events - Start collectors early: in
onCreatebefore any emissions - Use
repeatOnLifecycleto collect only when the UI is visible - Test timing-dependent flows with
runTestandTestDispatcher