SharedFlow: vad det är, SharedFlow vs StateFlow i Android

Författare: IT Sectr Publicerad: 2026-02-20 Lästid: 9 min

SharedFlow är en het reaktiv ström från biblioteket Kotlin Coroutines, optimerad för engångshändelser (one-shot events) som inte bör upprepas vid skärmrotation eller återskapande av prenumerant. Vi visar hur SharedFlow skiljer sig från StateFlow: till skillnad från StateFlow lagrar SharedFlow inte det senaste värdet för nya prenumeranter och stöder konfiguration av replay, extraBufferCapacity och onBufferOverflow. Enligt Google (Android Developers, 2025) är SharedFlow den rekommenderade lösningen för navigationskommandon, Snackbar-meddelanden och andra händelser som ska bearbetas exakt en gång.

Huvudpunkter

  • SharedFlow — hot flow för engångshändelser: nya prenumeranter får inte tidigare värden utan inställning av replay.
  • MutableSharedFlow — föränderlig version med metoderna emit() och tryEmit() för att skicka händelser.
  • SharedFlow vs StateFlow: SharedFlow slår inte samman värden (kan buffra flera), kräver inget startvärde, passar för engångshändelser.
  • replay — antal senaste händelser som spelas upp för ny prenumerant (standard 0).
  • extraBufferCapacity — extra buffert utöver replay som förhindrar blockering av emit().

Vad är SharedFlow i Kotlin?

SharedFlow är en het ström (hot flow) från biblioteket kotlinx.coroutines.flow som, till skillnad från StateFlow, inte är bunden till ett enda tillstånd och kan skicka ett godtyckligt antal händelser till godtyckliga prenumeranter. SharedFlow är basen för StateFlow — StateFlow är implementerat via SharedFlow med replay = 1.

Nyckelegenskapen för SharedFlow är att det inte behöver lagra det senaste värdet. Som standard (replay = 0) får en ny prenumerant ingenting förrän en ny händelse skickas. Detta gör SharedFlow idealiskt för scenarier där en händelse ska bearbetas exakt en gång: navigering, Snackbar, systemmeddelanden, QR-kodskanningsresultat.

SharedFlow stabiliserades i kotlinx.coroutines 1.4.0 (november 2020) tillsammans med StateFlow. Enligt Kotlin Coroutines dokumentation (2025) använder SharedFlow finkornig låsning för synkronisering av prenumeranter och ger linjär skalbarhet upp till 1000+ samtidiga prenumeranter utan prestandaförsämring, vilket bekräftats av JetBrains tester.

SharedFlow vs StateFlow: när ska vad användas

Valet mellan SharedFlow och StateFlow beror på semantiken hos datan som skickas: tillstånd (StateFlow) eller händelse (SharedFlow). Nedan följer tydliga kriterier med exempel.

KriteriumSharedFlowStateFlow
SemantikEngångshändelser (navigering, toast, alert)UI-tillstånd (lista, laddning, fel)
StartvärdeKrävs inteObligatoriskt
Upprepning vid prenumerationEndast om replay > 0Alltid senaste värdet
KonflationNej — händelser försvinner inte (om bufferten inte svämmar över)Ja — lagrar bara det senaste
BuffringKonfigurerbar via replay + extraBufferCapacityEndast 1 (replay=1 fast)
AnvändningnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

Enklaste regeln: om data ska visas vid skärmrotation — är det ett tillstånd (StateFlow). Om händelsen inte ska upprepas vid skärmrotation — är det en engångshändelse (SharedFlow). Till exempel «toast med felmeddelande» — SharedFlow: vid skärmrotation ska toasten inte visas igen. «Produktlista» — StateFlow: vid skärmrotation ska listan finnas kvar på skärmen.

På IT Sectr använder vi SharedFlow för: navigationskommandon (byte av skärm, öppning av deep link), UI-händelser (Snackbar, AlertDialog), systemmeddelanden (datauppdatering i bakgrunden, betalningsresultat), analytiska händelser (loggning, spårning).

MutableSharedFlow: emit, tryEmit och buffring

MutableSharedFlow är den föränderliga versionen av SharedFlow med metoderna emit() (suspend) och tryEmit() (non-suspend) för att skicka händelser. emit() pausas om bufferten är full och onBufferOverflow = SUSPEND. tryEmit() returnerar Boolean — om händelsen har lagts till i bufferten.

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
}

Konstruktorparametrarna är kritiska: replay = 0 garanterar att händelsen inte upprepas för en ny prenumerant; extraBufferCapacity = 10 — buffert vid snabb sändning av händelser innan UI har prenumererat; DROP_OLDEST — strategi vid överflöd: gamla händelser kasseras, nya bevaras. Enligt Kotlin Coroutines Performance (JetBrains, 2024) bearbetar SharedFlow med extraBufferCapacity = 64 mer än 100 000 händelser per sekund utan förlust.

SharedFlow för engångshändelser: Event-mönster

Event-mönster (eller UiEvent) — Googles rekommenderade sätt att överföra engångshändelser från ViewModel till View. Till skillnad från tillstånd (StateFlow) ska en händelse bearbetas exakt en gång och inte upprepas vid skärmrotation. SharedFlow med replay = 0 är idealiskt för denna uppgift.

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 ?: "Orderfel"))
            }
        }
    }
}

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

I View (Activity/Fragment): prenumeration på händelsen ska göras i lifecycleScope med repeatOnLifecycle(STATE.STARTED). Vid varje inträde i STARTED skapas prenumerationen på nytt, men händelsen upprepas inte eftersom SharedFlow med replay=0 redan har frigjort den. Detta garanterar att navigering till orderspårningsskärmen sker endast en gång, inte vid varje skärmrotation.

SharedFlow-parametrar: replay, extraBufferCapacity, onBufferOverflow

Konstruktorn för MutableSharedFlow tar tre parametrar som bestämmer buffertens beteende. Felaktig konfiguration kan leda till förlust av händelser eller blockering av emit().

ParameterTypStandardBeskrivning
replayInt0Antal senaste händelser som spelas upp för ny prenumerant. 0 = spela inte upp, 1 = som StateFlow
extraBufferCapacityInt0Extra buffert utöver replay. Händelser lagras i en cirkulär buffert. 64 — rekommenderad gräns för de flesta scenarier
onBufferOverflowBufferOverflowSUSPENDStrategi när bufferten är full: SUSPEND, DROP_OLDEST, DROP_LATEST
// Konfigurationer för olika scenarier:

// 1. Engångs UI-händelser (navigering, toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. Replay-ström för tillståndssynkronisering (som StateFlow)
val stateLike = MutableSharedFlow<AppState>(
    replay = 1,
    extraBufferCapacity = 0
)

// 3. Högfrekvent händelsesändning (analys, loggar)
val analytics = MutableSharedFlow<AnalyticsEvent>(
    replay = 0,
    extraBufferCapacity = 100,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

Viktigt: extraBufferCapacity + replay = total buffertstorlek. Om emit() anropas snabbare än prenumeranten bearbetar händelsen fylls bufferten och onBufferOverflow aktiveras. För UI-händelser är DROP_OLDEST en säker strategi: gamla händelser (inte längre relevanta navigeringar) kasseras till förmån för nya. För finansiella transaktioner, använd SUSPEND — detta garanterar att ingen händelse går förlorad, även till priset av att avsändaren blockeras.

Kodexempel: SharedFlow i Kotlin

Exempel 1: SharedFlow för navigering med Jetpack Navigation

Navigationskommandon är ett klassiskt användningsfall för SharedFlow. Fragment prenumererar på händelser och utför navigering. Vid skärmrotation upprepas inte kommandot.

// 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
}

// I 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()
            }
        }
    }
}

Exempel 2: SharedFlow med Room och Flow-operatorer

Komplext scenario: SharedFlow för bakgrundshändelsemeddelanden i kombination med StateFlow för UI.

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("Ny notis: ${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("Alla notiser har markerats som lästa")
        }
    }
}

I detta exempel: StateFlow lagrar meddelandelistan (tillstånd — bevaras vid skärmrotation), SharedFlow sänder toast-meddelanden (engångshändelser — upprepas inte vid skärmrotation). Kombinationen av två Flow-typer är Googles rekommenderade mönster för ViewModel från och med 2022.

Vanliga frågor

Kan SharedFlow förlora en händelse?

Ja, om bufferten svämmar över och onBufferOverflow = DROP_OLDEST eller DROP_LATEST. SharedFlow garanterar inte leverans av varje händelse — det är ingen meddelandekö (som Channel). Om garanterad leverans av alla händelser behövs, använd Channel med en buffert som inte svämmar över (UNLIMITED) eller BroadcastChannel (deprecated). För UI-händelser är förlust av föråldrade händelser (t.ex. gammal navigering) förväntat beteende, inte en bugg.

Hur skiljer sig SharedFlow från Channel?

Channel är en FIFO-kö där varje händelse levereras till exakt en prenumerant (punkt-till-punkt). SharedFlow är en sändning: varje händelse levereras till ALLA aktiva prenumeranter. SharedFlow liknar BroadcastChannel (som är deprecated) och passar för «en-till-många»-scenarier. Channel är för «en-till-en» (trådpooler, pipeline). Enligt JetBrains rekommendation är SharedFlow ersättning för BroadcastChannel för alla nya projekt.

Hur gör man SharedFlow trådsäker?

SharedFlow är redan trådsäkert — emit() och collect() är korrekt synkroniserade. Flera trådar kan anropa emit() utan låsning, och alla aktiva prenumeranter får händelser i rätt ordning. tryEmit() är icke-blockerande — returnerar false om bufferten är full. För högbelastade system, använd tryEmit() med DROP_OLDEST — detta förhindrar blockering av trådar.

Varför används inte SharedFlow för tillstånd?

SharedFlow utan replay=1 lagrar inte det senaste värdet — vid skärmrotation får den nya prenumeranten inte aktuellt tillstånd, UI förblir tomt. Med replay=1 beter sig SharedFlow som StateFlow men förlorar optimeringen av jämförelse via equals(), vilket orsakar onödiga meddelanden vid återsändning av samma värde. StateFlow är rätt val för tillstånd; SharedFlow för händelser.

Hur testar man SharedFlow?

För att testa SharedFlow, använd Turbine — Kotlin-biblioteket för att testa Flow. Turbine låter dig kontrollera varje emission separat med timeout och slutförandekontroll. Exempel: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Du kan också använda .toList() i runTest med angivelse av antalet förväntade händelser.

Sammanfattning

  • SharedFlow — het reaktiv ström för engångshändelser, inte bunden till senaste tillståndet.
  • SharedFlow vs StateFlow: SharedFlow — händelser (navigering, toasts, alertar), StateFlow — tillstånd (listor, laddning, fel).
  • MutableSharedFlow med replay=0, extraBufferCapacity=5, DROP_OLDEST — standardkonfiguration för UI-händelser.
  • emit() — suspend-funktion för blockerande sändning; tryEmit() — non-suspend med Boolean-resultat.
  • Mönstret UiEvent med sealed class — rekommenderat sätt att överföra engångshändelser från ViewModel till View.
  • SharedFlow garanterar leverans till varje prenumerant, men garanterar inte leverans av varje händelse vid buffertöverflöd.
  • Kombinationen SharedFlow + StateFlow i en ViewModel — optimalt mönster för arkitektur som separerar tillstånd och händelser.

Vi utvecklar en mobil applikation nyckelfärdigt

IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.

Diskutera projektet

Läs också