SharedFlow: What It Is, SharedFlow vs StateFlow in Android

Author: IT Sectr Published: 2026-02-20 Reading time: 9 min

SharedFlow is a hot reactive flow from the Kotlin Coroutines library, optimized for one-shot events that should not repeat on screen rotation or subscriber recreation. Let's show how SharedFlow differs from StateFlow: unlike StateFlow, SharedFlow does not store the last value for new subscribers and supports configuration of replay, extraBufferCapacity and onBufferOverflow. According to Google (Android Developers, 2025), SharedFlow is the recommended solution for navigation commands, Snackbar messages and other events that should be processed exactly once.

Key Takeaways

  • SharedFlow — hot flow for one-shot events: new subscribers do not receive previous values without replay configuration.
  • MutableSharedFlow — mutable version with emit() and tryEmit() methods for sending events.
  • SharedFlow vs StateFlow: SharedFlow does not conflate values (can buffer multiple values), does not require an initial value, suitable for one-shot events.
  • replay — number of recent events replayed to new subscribers (default 0).
  • extraBufferCapacity — additional buffer for events beyond replay, preventing emit() blocking.

What is SharedFlow in Kotlin?

SharedFlow is a hot flow from the kotlinx.coroutines.flow library, which, unlike StateFlow, is not tied to a single state and can emit an arbitrary number of events to arbitrary subscribers. SharedFlow is the base type for StateFlow — StateFlow is actually implemented via SharedFlow with replay = 1.

The key feature of SharedFlow is that it does not have to store the last value. By default (replay = 0), a new subscriber receives nothing until a new event is sent. This makes SharedFlow ideal for scenarios where an event should be processed exactly once: navigation, Snackbar, system notifications, QR code scan results.

SharedFlow was stabilized in kotlinx.coroutines 1.4.0 (November 2020) along with StateFlow. According to the Kotlin Coroutines documentation (2025), SharedFlow uses fine-grained locking for subscriber synchronization and provides linear scalability up to 1000+ concurrent subscribers without performance degradation, confirmed by JetBrains tests.

SharedFlow vs StateFlow: When to Use What

The choice between SharedFlow and StateFlow depends on the semantics of the data being transferred: state (StateFlow) or event (SharedFlow). Below are clear criteria with examples.

CriterionSharedFlowStateFlow
SemanticsOne-shot events (navigation, toast, alert)UI state (list, loading, error)
Initial valueNot requiredRequired
Replay on subscriptionOnly if replay > 0Always last value
ConflationNo — events are not lost (if buffer is not full)Yes — stores only the latest
BufferingConfigurable via replay + extraBufferCapacityOnly 1 (replay=1 fixed)
UsagenavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

The simplest rule: if the data should be shown on screen rotation — it's state (StateFlow). If on screen rotation the event should not repeat — it's a one-shot event (SharedFlow). For example, a toast with an error message is SharedFlow: on rotation the toast should not appear again. A product list is StateFlow: on rotation the list should remain on screen.

At IT Sectr we use SharedFlow for: navigation commands (screen transition, deep link opening), UI events (Snackbar, AlertDialog), system notifications (background data updates, payment result), analytics events (logging, tracking).

MutableSharedFlow: emit, tryEmit and Buffering

MutableSharedFlow is the mutable version of SharedFlow with emit() (suspend) and tryEmit() (non-suspend) methods for sending events. emit() suspends if the buffer is full and onBufferOverflow = SUSPEND. tryEmit() returns a Boolean indicating whether the event was successfully added to the buffer.

kotlin
class EventBus {
    private val _events = MutableSharedFlow<UiEvent>(
        replay = 0,
        extraBufferCapacity = 10,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val events: SharedFlow<UiEvent> get() = _events

    suspend fun sendEvent(event: UiEvent) {
        _events.emit(event)
    }

    fun trySendEvent(event: UiEvent): Boolean {
        return _events.tryEmit(event)
    }
}

sealed interface UiEvent {
    data class ShowSnackbar(val message: String) : UiEvent
    data class NavigateTo(val route: String) : UiEvent
    data class ShowDialog(val title: String, val message: String) : UiEvent
}

Constructor parameters are critically important: replay = 0 ensures the event does not repeat for a new subscriber; extraBufferCapacity = 10 provides a buffer for rapid event emission before the UI subscribes; DROP_OLDEST is the overflow strategy: old events are dropped, new ones are preserved. According to Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow with extraBufferCapacity = 64 processes over 100,000 events per second without loss.

SharedFlow for One-Shot Events: The Event Pattern

Event Pattern (or UiEvent) is Google's recommended way to pass one-shot events from ViewModel to View. Unlike state (StateFlow), an event should be processed exactly once, and on screen rotation it should not repeat. SharedFlow with replay = 0 is ideal for this task.

kotlin
class CheckoutViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
    val uiState: StateFlow<CheckoutState> get() = _uiState

    private val _event = MutableSharedFlow<CheckoutEvent>()
    val event: SharedFlow<CheckoutEvent> get() = _event

    fun placeOrder() {
        viewModelScope.launch {
            _uiState.value = CheckoutState.Loading
            try {
                val orderId = orderRepository.createOrder(cart)
                _uiState.value = CheckoutState.Success(orderId)
                _event.emit(CheckoutEvent.NavigateToOrderTracking(orderId))
            } catch (e: Exception) {
                _uiState.value = CheckoutState.Error(e.message)
                _event.emit(CheckoutEvent.ShowErrorSnackbar(e.message ?: "Layout error"))
            }
        }
    }
}

sealed interface CheckoutEvent {
    data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
    data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}

In the View (Activity/Fragment): subscription to events should be done in lifecycleScope with repeatOnLifecycle(STATE.STARTED). On each entry into STARTED, the subscription is recreated, but the event does not repeat because SharedFlow with replay=0 has already released it. This ensures that navigation to the order tracking screen happens only once, not on every rotation.

SharedFlow Parameters: replay, extraBufferCapacity, onBufferOverflow

The MutableSharedFlow constructor accepts three parameters that determine buffer behavior. Incorrect configuration can lead to event loss or emit() blocking.

ParameterTypeDefaultDescription
replayInt0Number of recent events replayed to a new subscriber. 0 = do not replay, 1 = like StateFlow
extraBufferCapacityInt0Additional buffer beyond replay. Events are stored in a circular buffer. 64 is the recommended limit for most scenarios
onBufferOverflowBufferOverflowSUSPENDStrategy when buffer is full: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// Configurations for different scenarios:

// 1. One-shot UI events (navigation, toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. Replay flow for state synchronization (like StateFlow)
val stateLike = MutableSharedFlow<AppState>(
    replay = 1,
    extraBufferCapacity = 0
)

// 3. High-frequency event emission (analytics, logs)
val analytics = MutableSharedFlow<AnalyticsEvent>(
    replay = 0,
    extraBufferCapacity = 100,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

Important: extraBufferCapacity + replay = total buffer size. If emit() is called faster than the subscriber processes events, the buffer fills up and onBufferOverflow triggers. For UI events, DROP_OLDEST is a safe strategy: old events (no longer relevant navigations) are dropped in favor of fresh ones. For financial transactions, use SUSPEND — this guarantees that no event is lost at the cost of blocking the sender.

Code Examples: SharedFlow in Kotlin

Example 1: SharedFlow for Navigation with Jetpack Navigation

Navigation commands are a classic use case for SharedFlow. A Fragment subscribes to events and performs navigation. On screen rotation, the command does not repeat.

kotlin
// ViewModel
class AuthViewModel : ViewModel() {
    private val _navEvent = MutableSharedFlow<NavEvent>()
    val navEvent: SharedFlow<NavEvent> get() = _navEvent

    fun onLoginSuccess() {
        viewModelScope.launch {
            _navEvent.emit(NavEvent.NavigateTo(NavRoutes.HOME))
        }
    }

    fun onLogout() {
        viewModelScope.launch {
            _navEvent.emit(NavEvent.NavigateTo(NavRoutes.LOGIN))
        }
    }
}

sealed interface NavEvent {
    data class NavigateTo(val route: String) : NavEvent
    data class NavigateBack(val popUpTo: String? = null) : NavEvent
}

// In Fragment:
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.navEvent.collect { navEvent ->
            when (navEvent) {
                is NavEvent.NavigateTo -> findNavController().navigate(navEvent.route)
                is NavEvent.NavigateBack -> findNavController().popBackStack()
            }
        }
    }
}

Example 2: SharedFlow with Room and Flow Operators

A complex scenario: SharedFlow for background event notifications combined with StateFlow for the UI.

kotlin
class NotificationViewModel : ViewModel() {
    private val _toastMessage = MutableSharedFlow<String>()
    val toastMessage: SharedFlow<String> get() = _toastMessage

    private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
    val notifications: StateFlow<List<Notification>> get() = _notifications

    init {
        viewModelScope.launch {
            notificationChannel
                .consumeAsFlow()
                .collect { notification ->
                    _notifications.value = _notifications.value + notification
                    _toastMessage.emit("New notification: ${notification.title}")
                }
        }
    }

    fun dismissNotification(id: String) {
        _notifications.value = _notifications.value.filter { it.id != id }
    }

    fun markAllRead() {
        viewModelScope.launch {
            _notifications.value = _notifications.value.map { it.copy(isRead = true) }
            _toastMessage.emit("All notifications marked as read")
        }
    }
}

In this example: StateFlow stores the notification list (state — preserved on rotation), SharedFlow emits toast messages (one-shot events — not repeated on rotation). The combination of two Flow types is Google's recommended pattern for ViewModel starting from 2022.

Frequently Asked Questions

Can SharedFlow lose an event?

Yes, if the buffer is full and onBufferOverflow = DROP_OLDEST or DROP_LATEST. SharedFlow does not guarantee delivery of every event — it is not a message queue (like Channel). If you need guaranteed delivery of all events, use Channel with an unlimited buffer (UNLIMITED) or BroadcastChannel (deprecated). For UI events, loss of outdated events (e.g., old navigation) is expected behavior, not a bug.

How is SharedFlow different from Channel?

Channel is a FIFO queue where each event is delivered to exactly one subscriber (point-to-point). SharedFlow is a broadcast: each event is delivered to ALL active subscribers. SharedFlow is closer to BroadcastChannel (which is deprecated) and is suitable for one-to-many scenarios. Channel is for one-to-one (thread pools, pipelines). According to JetBrains recommendation, SharedFlow is the replacement for BroadcastChannel in all new projects.

How to make SharedFlow thread-safe?

SharedFlow is already thread-safe — emit() and collect() are properly synchronized. Multiple threads can call emit() without locks, and all active subscribers receive events in the correct order. tryEmit() is non-blocking — it returns false if the buffer is full. For high-load systems, use tryEmit() with DROP_OLDEST — this prevents thread blocking.

Why isn't SharedFlow used for state?

SharedFlow without replay=1 does not store the last value — on screen rotation a new subscriber will not receive the current state, and the UI will remain empty. With replay=1, SharedFlow behaves like StateFlow but loses the equals() comparison optimization, causing unnecessary notifications when the same value is emitted again. StateFlow is the right choice for state; SharedFlow is for events.

How to test SharedFlow?

For testing SharedFlow, use Turbine — a Kotlin library for testing Flow. Turbine allows checking each emission individually with timeouts and completion verification. Example: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. You can also use .toList() in runTest specifying the number of expected events.

Summary

  • SharedFlow — a hot reactive flow for one-shot events not tied to the last state.
  • SharedFlow vs StateFlow: SharedFlow for events (navigation, toasts, alerts), StateFlow for state (lists, loading, errors).
  • MutableSharedFlow with replay=0, extraBufferCapacity=5, DROP_OLDEST is the standard configuration for UI events.
  • emit() — suspend function for blocking emission; tryEmit() — non-suspend with Boolean result.
  • The UiEvent with sealed class pattern is Google's recommended way to pass one-shot events from ViewModel to View.
  • SharedFlow guarantees delivery to each subscriber, but does not guarantee delivery of every event when the buffer overflows.
  • The combination of SharedFlow + StateFlow in a single ViewModel is the optimal pattern for architecture separating state and events.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also