- 🔁
MutableSharedFlowin Kotlin cannot emit "nothing"; every emission must carry a value like Unit or null. - ⭐ Kotlin
StateFlowis ideal for persistent state, whileSharedFlowfits transient, one-time events. - 📥 Replay cache in
SharedFlowcan cause unintentional re-delivery if not carefully configured. - ❌ Emissions may silently fail without active collectors or proper coroutine scope usage.
- ✅ Event wrappers like sealed classes prevent unintended repeated UI triggers on configuration changes.
Can MutableSharedFlow Emit Nothing in Kotlin?
Many people wonder what happens when you want MutableSharedFlow in Kotlin to "emit nothing." Developers building apps with Kotlin Coroutines often find this confusing, especially when they manage one-time events or signals in view models. Here, we will look at how MutableSharedFlow works, what "emit nothing" in Kotlin truly means, and how it stacks up against options like Kotlin StateFlow in actual app designs.
Understanding MutableSharedFlow in Kotlin
MutableSharedFlow is a type of SharedFlow. It came out as part of Kotlin’s Flow API and builds on top of StateFlow. It is a hot stream. It constantly sends out values that many observers can collect, no matter their lifecycle. Regular "cold" Flow streams only send values when a collector signs up. But SharedFlow (and MutableSharedFlow) sends values right away and without stopping. This happens even if no collectors are watching it.
Key Properties:
- Multicast: Many collectors can get the same emissions at the same time.
- Adjustable replay cache: You can pick how many events to save for collectors that join later.
- Adjustable buffer overflow: You decide what happens to emissions when the buffer is full.
- Does not suspend:
emit()will not pause if no collector is ready. This is different fromChannel.
Here is a simple example of how to use it:
val _events = MutableSharedFlow<MyEvent>()
val events: SharedFlow<MyEvent> = _events
This method is common in current Android design. It uses the MVVM pattern to send one-time UI events, such as navigation commands or showing Snackbars, from a ViewModel to the UI.
Clarifying “Emit Nothing” in Kotlin
The words “emit nothing” can mean a few things, depending on what your app does:
- I do not want anything sent out right now (this is a conditional skip).
- I want to show an intentional "null" or a command that does nothing.
- I want to clear or reset something without sending real data.
- I need to tell observers that nothing has changed. This is like a UI trigger with no data.
In truth, MutableSharedFlow cannot really emit “nothing.” The emit() function always needs an argument that is not empty and clearly defined. Because of this, patterns like sending Unit, null, sealed classes, or custom wrappers are key. They help developers show "emptiness" without breaking Kotlin's flow rules.
Why Kotlin Enforces Value Emissions
Kotlin's coroutines aim for strict type safety and reliable outcomes. Every emission in a flow, like SharedFlow, must send a clear value. This prevents confusion in environments with many subscribers running at the same time. Even if your emission is empty in concept, the Kotlin Flow API needs a real value. This stops mix-ups in designs for distributed systems or tough UI state management.
Emitting Minimal Signals:
val flow = MutableSharedFlow<Unit>()
flow.emit(Unit)
Here, Unit acts as a placeholder for “nothing.” It is empty in what it does, but it is valid to send out.
Why not let an actual empty emission happen? Because:
- It could lead to unexpected actions when many subscribers are present.
- It makes buffer and replay harder to manage.
- It might break type safety and guarantees about a collector's lifecycle.
The Unit Pattern: Efficient No-Op Emission
Unit is how Kotlin shows a result without a real value. It is much like void in Java, but Unit is more flexible. It is a usable object. It works well for signaling things like button clicks or simple lifecycle events.
Example: UI Trigger with Unit
private val _onClicked = MutableSharedFlow<Unit>()
val onClicked: SharedFlow<Unit> = _onClicked
fun handleClick() {
viewModelScope.launch {
_onClicked.emit(Unit)
}
}
Collectors use this event without needing to handle data:
lifecycleScope.launchWhenStarted {
viewModel.onClicked.collect {
showSnackbar("Clicked!")
}
}
When to Use Unit:
- 🔔 Start UI actions such as Toasts, Dialogs, or Navigation.
- ⚙️ Begin or end animation sequences.
- ✅ Tell about tasks that finished or worked.
But if your flows must carry meaning or error causes, Unit alone is not clear enough. You should use data types that show more.
Emitting Nulls: A Cautionary Option
MutableSharedFlow can send null values only if its type allows for null:
val _data = MutableSharedFlow<String?>()
_data.emit(null)
This works, but you should only use this pattern when null clearly means something on purpose. For example, it could mean clearing a state or showing that an item was taken out.
Problems with null values:
- They make code more complex because collectors need to check for
null. - They make code harder to read and test.
- They weaken the static type safety that Kotlin is good at.
Prefer null when:
- You have made
nullclearly mean an intentional case (for example, "no results"). - You always handle
nullchecks for all flow collectors. - Other options, such as sealed classes, are not possible.
Replay Cache and Event Re-delivery
A main problem when using MutableSharedFlow for one-time events is dealing with replays that you did not want. The replay cache controls this. It saves the last values sent out and sends them again for new collectors.
val _signal = MutableSharedFlow<Unit>(
replay = 1,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
If the cache holds past emissions (for instance, a button click), they might be sent again if a collector (like a Compose recomposition) starts over.
Pitfalls:
- Click actions or navigations happening again and again.
- Confusing UI effects (for example, Snackbars showing twice).
- Duplication from app lifecycle events that are hard to find and fix.
To stop these problems:
- Use
replay = 0for events that only happen once. - Handle state clearly with event wrappers.
- Make collectors mark events as "used up."
Event Wrappers for One-Time UI Events
To prevent duplicate emissions and make event handling safe when collectors restart, use custom models to show UI commands.
Sealed Class Example:
sealed class UiEvent {
object NavigateHome : UiEvent()
data class ShowToast(val message: String) : UiEvent()
object None : UiEvent()
}
Event Wrapper Pattern:
data class Event<out T>(val content: T, var handled: Boolean = false) {
fun getIfNotHandled(): T? = if (handled) null else {
handled = true
content
}
}
Both patterns help tell the difference between state that stays and commands that are used only once.
Kotlin StateFlow: When State Is More Than an Event
SharedFlow takes care of UI signals and events that pass quickly. But Kotlin StateFlow works better for screen states that stay around and continue.
- 🔄
StateFlowalways keeps a current value. - 🧩 It sends the latest value again to new collectors.
- 🧵 It fits well with Compose and LiveData patterns.
Example Usage:
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state
fun fetchData() {
viewModelScope.launch {
val result = repository.getData()
_state.value = UiState.Success(result)
}
}
Use StateFlow for these reasons:
- You need things to be steady when settings change (like screen rotation).
- The UI needs to show the most recent data.
- The state must always have a good default.
Do not use StateFlow for one-time triggers. This includes navigation events. It will send the value again to new collectors.
Best Practices: SharedFlow vs. StateFlow
| What you need to do | Best Flow to use |
|---|---|
| UI state updates | StateFlow |
| Button clicks / One-time events | SharedFlow |
| Signals that care about lifecycle | SharedFlow with Unit |
| Page transitions | Sealed SharedFlow |
| Form fill status | StateFlow |
Keeping aims separate in your ViewModel helps keep your design tidy:
// UI state
private val _uiState = MutableStateFlow<UiScreenState>(...)
val uiState: StateFlow<UiScreenState> = _uiState
// UI event
private val _showSnackbar = MutableSharedFlow<Unit>()
val showSnackbar: SharedFlow<Unit> = _showSnackbar
Avoiding Emission When Not Required
Sometimes, it is best not to emit anything.
Do not try to pretend a “null” or “empty” event happens if you do not need to. If an event should not take place, just do not call emit().
You can also show these "do nothing" states with sealed types:
sealed class LoadingUiEvent {
object None : LoadingUiEvent()
object ShowSpinner : LoadingUiEvent()
object HideSpinner : LoadingUiEvent()
}
Debugging Silent Emission Failures
If you send a value but nothing happens, think about these points:
- ❗ Are any collectors active?
- 💡 Is the collector part of a lifecycle (for example,
launchWhenStarted)? - 🎯 Are you sending the value too early when the app starts?
- ⏳ If there is no replay cache, did the collector sign up in time?
- 🧪 Check flows on their own with careful timing to see how emissions work.
Real-World Example: Button Click Handler
ViewModel:
private val _submitEvent = MutableSharedFlow<Unit>()
val submitEvent: SharedFlow<Unit> = _submitEvent
fun onSubmitClick() {
viewModelScope.launch {
_submitEvent.emit(Unit)
}
}
UI Layer:
lifecycleScope.launchWhenStarted {
viewModel.submitEvent.collect {
showSubmittingDialog()
}
}
If you used StateFlow here, the dialog would show again with every screen rotation. This would make the user experience choppy. SharedFlow, set up with care, makes sure the behavior is one-time and safe with the lifecycle.
Kotlin Best Practices in Jetpack Ecosystem
Google’s Jetpack design, which includes Navigation, Compose, and Hilt, helps Unidirectional Data Flow (UDF):
StateFlowhandles how the screen looks.SharedFlowhandles UI signals that interact and last for a short time.- Keeping things separate means code that is easy to keep up and test.
Kotlin Multiplatform and Compose Multiplatform are growing. So, these patterns now work for mobile, web, and desktop.
Conclusion: Choosing the Right Tool
It is key to know the fine points of MutableSharedFlow in Kotlin when you work with simple or symbolic events. If you want to "emit nothing" in Kotlin, instead, use Kotlin's strong features. These include Unit, sealed classes, or clear nulls, depending on what you are doing.
Use Kotlin StateFlow when you need state to stay, and do not use it for events that should not happen again. Take up structured patterns like event wrappers for UI triggers that happen once. And always think about buffer and lifecycle management.
Your design's clarity often relies on how well you pick between these tools.
Citations
JetBrains. (n.d.). Kotlin Coroutine Reference.
Reactive Manifesto. (n.d.). Principles of Reactive Systems.