StateFlow vs SharedFlow in Android: Complete Guide

StateFlow and SharedFlow are hot Flow APIs used to expose values from long-lived Android components such as a ViewModel. They share a foundation, but they model different things: StateFlow represents the latest state; SharedFlow broadcasts values to current and, optionally, future subscribers.

What is Flow?

A Kotlin Flow is an asynchronous stream. A regular flow { } is cold: its producer starts separately for each collector. StateFlow and SharedFlow are hot, so their lifetime is independent of any single collector.

What is StateFlow?

StateFlow is a read-only state holder with exactly one current value. MutableStateFlow requires an initial value, replays the latest value to a new collector, and suppresses updates that are equal according to Any.equals.

data class ProfileUiState(
    val loading: Boolean = false,
    val name: String = "",
    val error: String? = null
)

private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()

What is SharedFlow?

SharedFlow is a configurable broadcast stream. It has no required initial value. Replay and extra buffer capacity can be selected when MutableSharedFlow is created.

sealed interface ProfileEvent {
    data object Saved : ProfileEvent
    data class Message(val text: String) : ProfileEvent
}

private val _events = MutableSharedFlow<ProfileEvent>(
    extraBufferCapacity = 1
)
val events: SharedFlow<ProfileEvent> = _events.asSharedFlow()

Lifecycle behavior and hot streams

Neither type stops existing because an Activity or composable leaves the screen. The owner controls the lifetime. Collection must still be lifecycle-aware: use repeatOnLifecycle in Views or collectAsStateWithLifecycle for state in Compose.

Replay and initial value

Feature StateFlow SharedFlow
Best model Current UI state Broadcast values/events
Initial value Required Not required
Replay Always latest value Configurable, default 0
Current value value No single value
Equality conflation Yes No
Buffer Conflated state Configurable

StateFlow for UI state in a ViewModel

class ProfileViewModel(
    private val repository: ProfileRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(ProfileUiState())
    val uiState = _uiState.asStateFlow()

    fun load() = viewModelScope.launch {
        _uiState.update { it.copy(loading = true, error = null) }
        runCatching { repository.loadProfile() }
            .onSuccess { profile ->
                _uiState.value = ProfileUiState(name = profile.name)
            }
            .onFailure { throwable ->
                _uiState.value = ProfileUiState(error = throwable.message ?: "Unable to load profile")
            }
    }
}

Collecting StateFlow in Jetpack Compose

@Composable
fun ProfileRoute(viewModel: ProfileViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileScreen(state = state, onRetry = viewModel::load)
}

The lifecycle-runtime-compose artifact provides collectAsStateWithLifecycle. It collects while the lifecycle is active and exposes the latest value as Compose state.

SharedFlow for one-time events

A SharedFlow can deliver transient signals such as navigation or a snackbar, but “one-time event” delivery is not automatically guaranteed. With replay 0, an event emitted while there is no subscriber is missed. If the event must survive recreation or process death, model it as state or persist it.

fun save() = viewModelScope.launch {
    repository.saveProfile(_uiState.value.name)
    _events.emit(ProfileEvent.Saved)
}

Collecting events in Compose

@Composable
fun ProfileRoute(
    viewModel: ProfileViewModel = viewModel(),
    onSaved: () -> Unit
) {
    val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(viewModel, lifecycleOwner) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.events.collect { event ->
                when (event) {
                    ProfileEvent.Saved -> onSaved()
                    is ProfileEvent.Message -> Unit
                }
            }
        }
    }
}

Complete state and event example

A practical screen uses StateFlow for everything needed to render again and SharedFlow only for best-effort transient effects. Keep mutable flows private, expose read-only types, and update StateFlow atomically with update.

Common mistakes

  • Using SharedFlow with replay 0 for information that must not be lost.
  • Encoding a snackbar or navigation command permanently in state without marking it handled.
  • Exposing MutableStateFlow or MutableSharedFlow publicly.
  • Collecting in a View lifecycle without repeatOnLifecycle.
  • Mutating an object already stored in StateFlow instead of assigning an immutable copy.
  • Assuming extra buffer capacity makes delivery durable.

Interview questions

Why does StateFlow require an initial value?

It always represents a current state, including before the first asynchronous result arrives.

Can SharedFlow behave like StateFlow?

A replay value of one is similar, but SharedFlow has no value property and does not apply StateFlow equality conflation. Use StateFlow when the abstraction is state.

Does StateFlow respect the Android lifecycle?

It is lifecycle-independent. The collection API determines when an Android UI observes it.

Conclusion

Choose StateFlow for durable, renderable UI state and SharedFlow for broadcast values whose replay and buffering you intentionally configure. For critical actions, represent pending work as state or store it durably rather than depending on an ephemeral event.

Leave a Comment

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

Scroll to Top