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 ä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.
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.
| Kriterium | SharedFlow | StateFlow |
|---|---|---|
| Semantik | Engångshändelser (navigering, toast, alert) | UI-tillstånd (lista, laddning, fel) |
| Startvärde | Krävs inte | Obligatoriskt |
| Upprepning vid prenumeration | Endast om replay > 0 | Alltid senaste värdet |
| Konflation | Nej — händelser försvinner inte (om bufferten inte svämmar över) | Ja — lagrar bara det senaste |
| Buffring | Konfigurerbar via replay + extraBufferCapacity | Endast 1 (replay=1 fast) |
| Användning | navigationEvent, showSnackbar, openDialog | items, 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 ä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.
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.
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().
| Parameter | Typ | Standard | Beskrivning |
|---|---|---|---|
| replay | Int | 0 | Antal senaste händelser som spelas upp för ny prenumerant. 0 = spela inte upp, 1 = som StateFlow |
| extraBufferCapacity | Int | 0 | Extra buffert utöver replay. Händelser lagras i en cirkulär buffert. 64 — rekommenderad gräns för de flesta scenarier |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategi 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.
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()
}
}
}
}
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
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.
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.
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.
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.
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
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.
Läs också