SharedFlow è un flusso reattivo caldo della libreria Kotlin Coroutines, ottimizzato per eventi una tantum (one-shot events) che non dovrebbero ripetersi alla rotazione dello schermo o alla ricreazione dell'abbonato. Mostriamo come SharedFlow differisce da StateFlow: a differenza di StateFlow, SharedFlow non memorizza l'ultimo valore per i nuovi abbonati e supporta la configurazione di replay, extraBufferCapacity e onBufferOverflow. Secondo Google (Android Developers, 2025), SharedFlow è la soluzione consigliata per comandi di navigazione, messaggi Snackbar e altri eventi che devono essere elaborati esattamente una volta.
Punti Chiave
SharedFlow è un flusso caldo (hot flow) della libreria kotlinx.coroutines.flow che, a differenza di StateFlow, non è legato a un singolo stato e può emettere un numero arbitrario di eventi a abbonati arbitrari. SharedFlow è il tipo base per StateFlow — StateFlow è infatti implementato tramite SharedFlow con replay = 1.
La caratteristica principale di SharedFlow è che non deve memorizzare l'ultimo valore. Per impostazione predefinita (replay = 0), un nuovo abbonato non riceve nulla finché non viene inviato un nuovo evento. Ciò rende SharedFlow ideale per scenari in cui un evento deve essere elaborato esattamente una volta: navigazione, Snackbar, notifiche di sistema, risultati di scansione di codici QR.
SharedFlow è stato stabilizzato in kotlinx.coroutines 1.4.0 (novembre 2020) insieme a StateFlow. Secondo la documentazione di Kotlin Coroutines (2025), SharedFlow utilizza un blocco a grana fine per la sincronizzazione degli abbonati e fornisce scalabilità lineare fino a oltre 1000 abbonati simultanei senza degrado delle prestazioni, confermato dai test JetBrains.
La scelta tra SharedFlow e StateFlow dipende dalla semantica dei dati trasferiti: stato (StateFlow) o evento (SharedFlow). Di seguito sono riportati criteri chiari con esempi.
| Criterio | SharedFlow | StateFlow |
|---|---|---|
| Semantica | Eventi una tantum (navigazione, toast, avviso) | Stato dell'UI (lista, caricamento, errore) |
| Valore iniziale | Non richiesto | Richiesto |
| Riproduzione all'abbonamento | Solo se replay > 0 | Sempre l'ultimo valore |
| Conflazione | No — gli eventi non vengono persi (se il buffer non è pieno) | Sì — memorizza solo l'ultimo |
| Bufferizzazione | Configurabile tramite replay + extraBufferCapacity | Solo 1 (replay=1 fisso) |
| Utilizzo | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
La regola più semplice: se i dati devono essere mostrati alla rotazione dello schermo — è stato (StateFlow). Se alla rotazione dello schermo l'evento non deve ripetersi — è un evento una tantum (SharedFlow). Ad esempio, un toast con un messaggio di errore è SharedFlow: alla rotazione, il toast non deve apparire di nuovo. Un elenco di prodotti è StateFlow: alla rotazione, l'elenco deve rimanere sullo schermo.
In IT Sectr utilizziamo SharedFlow per: comandi di navigazione (transizione schermo, apertura deep link), eventi UI (Snackbar, AlertDialog), notifiche di sistema (aggiornamenti dati in background, risultato pagamento), eventi di analisi (logging, tracciamento).
MutableSharedFlow è la versione mutabile di SharedFlow con i metodi emit() (suspend) e tryEmit() (non-suspend) per l'invio di eventi. emit() si sospende se il buffer è pieno e onBufferOverflow = SUSPEND. tryEmit() restituisce un Boolean indicando se l'evento è stato aggiunto con successo al buffer.
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
}
I parametri del costruttore sono criticamente importanti: replay = 0 garantisce che l'evento non si ripeta per un nuovo abbonato; extraBufferCapacity = 10 fornisce un buffer per l'emissione rapida di eventi prima che l'UI si abboni; DROP_OLDEST è la strategia di overflow: gli eventi vecchi vengono scartati, quelli nuovi vengono conservati. Secondo Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow con extraBufferCapacity = 64 elabora oltre 100.000 eventi al secondo senza perdite.
Pattern Event (o UiEvent) è il metodo consigliato da Google per passare eventi una tantum da ViewModel a View. A differenza dello stato (StateFlow), un evento deve essere elaborato esattamente una volta e, alla rotazione dello schermo, non deve ripetersi. SharedFlow con replay = 0 è ideale per questo compito.
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 ?: "Errore di layout"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
Nella View (Activity/Fragment): l'abbonamento agli eventi dovrebbe essere fatto in lifecycleScope con repeatOnLifecycle(STATE.STARTED). Ad ogni ingresso in STARTED, l'abbonamento viene ricreato, ma l'evento non si ripete perché SharedFlow con replay=0 lo ha già rilasciato. Ciò garantisce che la navigazione verso la schermata di tracciamento dell'ordine avvenga solo una volta, non ad ogni rotazione.
Il costruttore di MutableSharedFlow accetta tre parametri che determinano il comportamento del buffer. Una configurazione errata può portare alla perdita di eventi o al blocco di emit().
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
| replay | Int | 0 | Numero di eventi recenti riprodotti a un nuovo abbonato. 0 = non riprodurre, 1 = come StateFlow |
| extraBufferCapacity | Int | 0 | Buffer aggiuntivo oltre replay. Gli eventi sono memorizzati in un buffer circolare. 64 è il limite consigliato per la maggior parte degli scenari |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategia quando il buffer è pieno: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Configurazioni per diversi scenari:
// 1. Eventi UI una tantum (navigazione, toast)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Flusso di replay per sincronizzazione stato (come StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Emissione eventi ad alta frequenza (analisi, log)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Importante: extraBufferCapacity + replay = dimensione totale del buffer. Se emit() viene chiamato più velocemente di quanto l'abbonato elabori gli eventi, il buffer si riempie e onBufferOverflow si attiva. Per gli eventi UI, DROP_OLDEST è una strategia sicura: gli eventi vecchi (navigazioni non più rilevanti) vengono scartati a favore di quelli nuovi. Per le transazioni finanziarie, utilizzare SUSPEND — ciò garantisce che nessun evento venga perso al costo di bloccare il mittente.
I comandi di navigazione sono un caso d'uso classico per SharedFlow. Un Fragment si abbona agli eventi ed esegue la navigazione. Alla rotazione dello schermo, il comando non si ripete.
// 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
}
// Nel 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()
}
}
}
}
Uno scenario complesso: SharedFlow per notifiche di eventi in background combinato con StateFlow per l'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("Nuova notifica: ${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("Tutte le notifiche segnate come lette")
}
}
}
In questo esempio: StateFlow memorizza l'elenco delle notifiche (stato — preservato alla rotazione), SharedFlow emette messaggi toast (eventi una tantum — non ripetuti alla rotazione). La combinazione di due tipi di Flow è il pattern raccomandato da Google per ViewModel a partire dal 2022.
Domande Frequenti
Sì, se il buffer è pieno e onBufferOverflow = DROP_OLDEST o DROP_LATEST. SharedFlow non garantisce la consegna di ogni evento — non è una coda di messaggi (come Channel). Se hai bisogno di una consegna garantita di tutti gli eventi, usa Channel con buffer illimitato (UNLIMITED) o BroadcastChannel (deprecato). Per gli eventi UI, la perdita di eventi obsoleti (ad esempio, vecchia navigazione) è un comportamento previsto, non un bug.
Channel è una coda FIFO in cui ogni evento viene consegnato a esattamente un abbonato (punto-a-punto). SharedFlow è una trasmissione: ogni evento viene consegnato a TUTTI gli abbonati attivi. SharedFlow è più vicino a BroadcastChannel (che è deprecato) ed è adatto per scenari uno-a-molti. Channel è per uno-a-uno (pool di thread, pipeline). Secondo la raccomandazione di JetBrains, SharedFlow è il sostituto di BroadcastChannel in tutti i nuovi progetti.
SharedFlow è già thread-safe — emit() e collect() sono correttamente sincronizzati. Più thread possono chiamare emit() senza blocchi e tutti gli abbonati attivi ricevono gli eventi nell'ordine corretto. tryEmit() non è bloccante — restituisce false se il buffer è pieno. Per i sistemi ad alto carico, usa tryEmit() con DROP_OLDEST — questo previene il blocco dei thread.
SharedFlow senza replay=1 non memorizza l'ultimo valore — alla rotazione dello schermo, un nuovo abbonato non riceverà lo stato corrente e l'UI rimarrà vuota. Con replay=1, SharedFlow si comporta come StateFlow ma perde l'ottimizzazione del confronto tramite equals(), causando notifiche non necessarie quando lo stesso valore viene emesso di nuovo. StateFlow è la scelta giusta per lo stato; SharedFlow è per gli eventi.
Per testare SharedFlow, usa Turbine — una libreria Kotlin per testare Flow. Turbine permette di verificare ogni emissione singolarmente con timeout e verifica del completamento. Esempio: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Puoi anche usare .toList() in runTest specificando il numero di eventi attesi.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche