Introduction

MutableSharedFlow broadcasts values to multiple collectors, but unlike StateFlow, it does not have a default replay value. Collectors that start after an emission has occurred miss that value entirely. Additionally, buffer overflow policies and collector subscription timing can cause emissions to be silently dropped. This is a common source of missed UI updates in ViewModel-to-View communication.

Symptoms

  • Event emitted but UI does not update
  • Collector receives some emissions but not others
  • First emission always missed after screen rotation
  • SharedFlow buffer full causing BufferOverflow.SUSPEND deadlock
  • Multiple collectors but only one receives emissions

Debug SharedFlow: ```kotlin val sharedFlow = MutableSharedFlow<String>( replay = 0, extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST )

// Monitor emissions lifecycleScope.launch { sharedFlow.onEach { event -> Log.d("SharedFlow", "Received: $event") }.launchIn(lifecycleScope) } ```

Common Causes

  • replay = 0 means late collectors miss prior emissions
  • Buffer overflow dropping events when collector is slow
  • Collector lifecycle shorter than emission source
  • SharedFlow used for one-time events instead of Channel
  • Multiple subscriptions creating duplicate processing

Step-by-Step Fix

  1. 1.**Use replay to ensure collectors get latest value":
  2. 2.```kotlin
  3. 3.// WRONG - replay = 0, late collectors miss everything
  4. 4.val events = MutableSharedFlow<UiEvent>(replay = 0)

// CORRECT - replay = 1, collectors get the latest event val events = MutableSharedFlow<UiEvent>( replay = 1, // Replay last emission to new collectors extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST )

// For StateFlow-like behavior (always has a value): val state = MutableStateFlow<UiState>(UiState.Loading) // StateFlow always has replay = 1 and initial value built in ```

  1. 1.**Handle one-time events with Channel instead of SharedFlow":
  2. 2.```kotlin
  3. 3.// SharedFlow is for continuous streams, NOT for one-time events
  4. 4.// WRONG - SharedFlow for navigation event
  5. 5.val navigationEvents = MutableSharedFlow<NavigationEvent>()
  6. 6.// Problem: replay may cause duplicate navigation on config change

// CORRECT - Channel for one-time events val navigationChannel = Channel<NavigationEvent>(Channel.BUFFERED)

// In ViewModel fun onItemClicked(id: String) { viewModelScope.launch { navigationChannel.send(NavigationEvent.Detail(id)) } }

// In View lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.navigationChannel.consumeAsFlow().collect { event -> navigate(event) } } } ```

  1. 1.**Fix buffer overflow causing emission loss":
  2. 2.```kotlin
  3. 3.// WRONG - buffer full, emissions dropped
  4. 4.val events = MutableSharedFlow<String>(
  5. 5.replay = 0,
  6. 6.extraBufferCapacity = 1,
  7. 7.onBufferOverflow = BufferOverflow.DROP_OLDEST
  8. 8.)

// Emit faster than collector processes -> events lost repeat(100) { events.emit("Event $it") } // Only last 1 received

// CORRECT - use SUSPEND to apply backpressure val events = MutableSharedFlow<String>( replay = 0, extraBufferCapacity = 64, onBufferOverflow = BufferOverflow.SUSPEND // Waits for collector )

// Or use DROP_LATEST to keep processing old events val events = MutableSharedFlow<String>( replay = 0, extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_LATEST // Keep old, drop new ) ```

  1. 1.**Ensure collector lifecycle matches emission source":
  2. 2.```kotlin
  3. 3.class MyViewModel : ViewModel() {
  4. 4.private val _events = MutableSharedFlow<UiEvent>(replay = 1)
  5. 5.val events = _events.asSharedFlow()

fun triggerEvent(event: UiEvent) { viewModelScope.launch { _events.emit(event) } } }

class MyFragment : Fragment() { private val viewModel: MyViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState)

// CORRECT - collect with lifecycle awareness viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.events.collect { event -> handleEvent(event) } } } } } ```

Prevention

  • Use StateFlow for state that always has a current value
  • Use SharedFlow with replay = 1 for event streams where late collectors need the latest
  • Use Channel for one-time events (navigation, snackbars)
  • Set extraBufferCapacity based on expected emission rate
  • Use repeatOnLifecycle to manage collector lifecycle in Android
  • Test SharedFlow behavior with delayed collectors in unit tests