Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Can MutableSharedFlow Emit Nothing in Kotlin?

Wondering if MutableSharedFlow can emit nothing in Kotlin? Learn the alternatives and when to use Unit or StateFlow in view models.
Kotlin developer confused over MutableSharedFlow emit nothing with glitchy code visualization Kotlin developer confused over MutableSharedFlow emit nothing with glitchy code visualization
  • 🔁 MutableSharedFlow in Kotlin cannot emit "nothing"; every emission must carry a value like Unit or null.
  • ⭐ Kotlin StateFlow is ideal for persistent state, while SharedFlow fits transient, one-time events.
  • 📥 Replay cache in SharedFlow can 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 from Channel.

Here is a simple example of how to use it:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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:

  1. I do not want anything sent out right now (this is a conditional skip).
  2. I want to show an intentional "null" or a command that does nothing.
  3. I want to clear or reset something without sending real data.
  4. 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 null clearly mean an intentional case (for example, "no results").
  • You always handle null checks 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 = 0 for 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.

  • 🔄 StateFlow always 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):

  • StateFlow handles how the screen looks.
  • SharedFlow handles 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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading