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
SharedFlowbuffer full causingBufferOverflow.SUSPENDdeadlock- 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 = 0means late collectors miss prior emissions- Buffer overflow dropping events when collector is slow
- Collector lifecycle shorter than emission source
SharedFlowused for one-time events instead ofChannel- Multiple subscriptions creating duplicate processing
Step-by-Step Fix
- 1.**Use replay to ensure collectors get latest value":
- 2.```kotlin
- 3.// WRONG - replay = 0, late collectors miss everything
- 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.**Handle one-time events with Channel instead of SharedFlow":
- 2.```kotlin
- 3.// SharedFlow is for continuous streams, NOT for one-time events
- 4.// WRONG - SharedFlow for navigation event
- 5.val navigationEvents = MutableSharedFlow<NavigationEvent>()
- 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.**Fix buffer overflow causing emission loss":
- 2.```kotlin
- 3.// WRONG - buffer full, emissions dropped
- 4.val events = MutableSharedFlow<String>(
- 5.replay = 0,
- 6.extraBufferCapacity = 1,
- 7.onBufferOverflow = BufferOverflow.DROP_OLDEST
- 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.**Ensure collector lifecycle matches emission source":
- 2.```kotlin
- 3.class MyViewModel : ViewModel() {
- 4.private val _events = MutableSharedFlow<UiEvent>(replay = 1)
- 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
StateFlowfor state that always has a current value - Use
SharedFlowwithreplay = 1for event streams where late collectors need the latest - Use
Channelfor one-time events (navigation, snackbars) - Set
extraBufferCapacitybased on expected emission rate - Use
repeatOnLifecycleto manage collector lifecycle in Android - Test SharedFlow behavior with delayed collectors in unit tests