SharedFlow & StateFlow — Hot Streams for UI and API Systems

Kotlin’s SharedFlow and StateFlow are two of the most important building blocks for modern Android and backend development. While Kotlin’s Flow API provides elegant, coroutine-based cold streams, real applications often need hot streams — streams that emit values even when no one is listening.

Think about:

  • UI state that must always have a current value
  • Navigation events
  • Push notifications
  • Multi-subscriber event broadcasts
  • Replaying initial values to new collectors
  • System-wide updates from an API or socket

Cold flows cannot handle these cases alone.
This is where StateFlow and SharedFlow come in.

In this article, we’ll explore:

  1. Why LiveData is not enough
  2. Differences between SharedFlow & StateFlow
  3. Replay, buffer, and state management
  4. Using StateFlow in ViewModels
  5. Using SharedFlow for event handling
  6. API use cases: broadcast events & messaging systems

By the end, you’ll understand exactly when to use each type.


Why LiveData Is Not Enough

Before Flow became standard in Android, LiveData was the recommended solution for reactive UIs. While LiveData is still useful, it has several limitations.


1. LiveData is Lifecycle-bound (and only for Android)

  • Cannot be used in backend systems
  • Cannot be used in pure Kotlin modules
  • Cannot be shared across layers like repositories or data sources
  • Cannot broadcast to non-UI consumers

LiveData’s lifecycle-awareness is useful, but restrictive.


2. LiveData only has 1 value at a time

LiveData stores only the latest value and cannot replay older emissions.


3. LiveData is not coroutine-friendly

Yes, there are integrations now (liveData {}), but the API was not designed around coroutines.

Flow, StateFlow, and SharedFlow are coroutine-first and therefore integrate more naturally with:

  • suspend functions
  • structured concurrency
  • coroutine dispatchers
  • cancellation

4. LiveData does not support backpressure or reactive operators

Flow has dozens of operators (map, filter, take, flatMapLatest etc.), while LiveData transformations are limited.


5. LiveData cannot handle event streams properly

Navigation events, error messages, or “once-only” actions often require hacks:

  • SingleLiveEvent pattern
  • Event wrappers
  • Mediator LiveData tricks

SharedFlow solves this cleanly.


Differences Between SharedFlow & StateFlow

StateFlow and SharedFlow are both hot flows, but they serve different purposes.

Let’s compare them.


What is SharedFlow?

SharedFlow is a hot, multicast Flow that broadcasts emissions to all subscribers.
It does not hold any state (unless configured).

Key characteristics:

  • No initial value
  • No required current state
  • Can replay old values to new subscribers
  • Multiple collectors receive the same values
  • Ideal for events, signals, broadcasts, notifications

What is StateFlow?

StateFlow is a hot, state-holder Flow.
It always has a current value (value) and always emits the latest state to new collectors.

Key characteristics:

  • Has an initial value
  • Always holds exactly one latest state
  • New subscribers immediately receive the current value
  • Perfect for UI state models

StateFlow is essentially SharedFlow + state.


Comparison Table

FeatureStateFlowSharedFlow
Holds state?✅ Yes❌ No
Initial value required?✅ Yes❌ No
Replays latest value?AlwaysOptional (replay parameter)
Ideal forUI stateEvents
Value accessible without collecting?Yes (stateFlow.value)No
Can have multiple collectors?YesYes
Behavior when no collectors?Continues emittingContinues emitting

Replay, Buffer, and State Management

SharedFlow is highly configurable, and understanding replay/buffer options is key to choosing the right tool.


Replay

Replay determines how many old values a new subscriber will immediately receive.

Example:

val shared = MutableSharedFlow<Int>(replay = 2)

If we emit:

emit(1)
emit(2)
emit(3)

A new collector will receive:

2
3

When to use replay?

  • When new collectors must catch up
  • For chat histories, logs, or sensor data
  • For multi-layer pipelines (repository → ViewModel → UI)

Buffer

SharedFlow also supports extraBufferCapacity.

MutableSharedFlow<Int>(
    replay = 0,
    extraBufferCapacity = 10
)

This allows:

  • Producers to continue emitting even when consumers are slow
  • Prevents backpressure errors

When buffer fills up, SharedFlow can:

  • suspend the emitter
  • drop oldest values
  • drop latest values
  • depending on onBufferOverflow settings

StateFlow and State Management

StateFlow is designed for UI state:

private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

Properties:

  • _uiState.value updates state immediately
  • collectors always receive current value
  • used heavily in ViewModel → UI communication

StateFlow is perfect for:

  • screens with multiple states (loading, success, error)
  • Compose or XML data binding
  • sending state updates to many collectors

You can think of it like:

BehaviorSubject in RxJava
or
LiveData but better


Using StateFlow in ViewModels

StateFlow is now recommended instead of LiveData for UI state.

Example: UI State Model

data class UserUiState(
    val loading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

ViewModel using StateFlow

class UserViewModel(
    private val repo: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow(UserUiState())
    val state: StateFlow<UserUiState> = _state

    fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = _state.value.copy(loading = true)

            try {
                val user = repo.loadUser(id)
                _state.value = UserUiState(
                    loading = false, user = user
                )
            } catch (e: Exception) {
                _state.value = UserUiState(
                    loading = false, error = e.message
                )
            }
        }
    }
}

UI Collecting StateFlow

Compose:

val uiState by viewModel.state.collectAsState()

XML/Fragment:

lifecycleScope.launchWhenStarted {
    viewModel.state.collect { state ->
        render(state)
    }
}

StateFlow makes UI reactive, clean, and consistent.


SharedFlow for Event Handling

Events are different from state:

  • navigation
  • error messages
  • snackbars
  • one-time commands

Using StateFlow for events is wrong, because StateFlow replays the current state every time, causing events to repeat.

SharedFlow solves this.


Example: navigation event

private val _events = MutableSharedFlow<UserEvent>()
val events = _events.asSharedFlow()

Emit event:

viewModelScope.launch {
    _events.emit(UserEvent.NavigateToProfile)
}

Collect event:

lifecycleScope.launchWhenStarted {
    viewModel.events.collect { event ->
        when(event) {
            NavigateToProfile -> navController.navigate("profile")
        }
    }
}

Because SharedFlow does NOT hold state:

  • navigation won’t repeat
  • backstack changes won’t retrigger
  • collectors get exactly what is emitted

Replay = 0 for events

Events should not replay unless explicitly required.

This is a cleaner alternative to:

  • SingleLiveEvent
  • Event wrappers
  • Channels

SharedFlow for multi-subscriber events

Multiple screens or layers can listen:

  • logging layer
  • analytics layer
  • UI layer

All receive the same event.


API Use Cases (Broadcast Events, Message Services)

SharedFlow is not only for UI. In backend or service layers, SharedFlow becomes extremely powerful.


1. Broadcast system-wide API events

Example: API sends updates when system config changes.

val configUpdates = MutableSharedFlow<Config>(replay = 1)

Producers:

launch {
    while(true) {
        val config = api.loadConfig()
        configUpdates.emit(config)
        delay(5_000)
    }
}

Consumers:

launch {
    configUpdates.collect { update ->
        println("Config changed: $update")
    }
}

With replay = 1, new collectors immediately receive the latest config.


2. Message buses between layers

For example, repository broadcasting updates to use cases or ViewModels.

val messageBus = MutableSharedFlow<Event>(extraBufferCapacity = 10)

Components send messages:

messageBus.tryEmit(Event.DataUpdated)

Multiple collectors respond:

  • UI
  • Logging system
  • Background processor

3. Realtime chat or notification systems

Use replay to deliver past messages:

val chatFlow = MutableSharedFlow<Message>(replay = 100)

New users joining a chat UI instantly see the last 100 messages.


4. WebSocket or MQTT broadcast

When handling socket messages:

  • one producer
  • many consumers

SharedFlow matches perfectly.


Full Example: UI + Events + API Broadcast

Let’s combine everything.


ViewModel: State + Events

class DashboardViewModel(
    private val api: Api
) : ViewModel() {

    private val _state = MutableStateFlow(DashboardState())
    val state = _state.asStateFlow()

    private val _events = MutableSharedFlow<DashboardEvent>()
    val events = _events.asSharedFlow()

    fun refresh() {
        viewModelScope.launch {
            _state.update { it.copy(loading = true) }

            try {
                val data = api.loadDashboard()
                _state.update { it.copy(loading = false, data = data) }
            } catch (e: Exception) {
                _events.emit(DashboardEvent.ShowError(e.message ?: "Unknown error"))
                _state.update { it.copy(loading = false) }
            }
        }
    }
}


UI Layer

Collect state:

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
    viewModel.state.collect { render(it) }
}

Collect events:

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
    viewModel.events.collect { event ->
        when (event) {
            is DashboardEvent.ShowError -> showToast(event.message)
        }
    }
}

State and events remain distinct and predictable.


Conclusion

SharedFlow and StateFlow are essential building blocks of modern Kotlin reactive programming. They bring powerful, hot-stream behavior that works seamlessly with coroutines.

You learned:

  • Why LiveData is not enough
  • Differences between StateFlow and SharedFlow
  • Replay and buffer mechanics
  • Using StateFlow to model UI state
  • Using SharedFlow for once-off events
  • How SharedFlow powers broadcasting, messaging, and real-time systems

Key takeaways

  • StateFlow = state holder
  • SharedFlow = event broadcaster
  • Both are hot, coroutine-driven streams
  • They replace old LiveData and event-wrapper hacks
  • SharedFlow is ideal for multi-subscriber systems
  • StateFlow is perfect for UI state, Compose, and reactive rendering

Leave a Reply

Your email address will not be published. Required fields are marked *