SharedFlow: cos'è, SharedFlow vs StateFlow in Android

Autore: IT Sectr Pubblicato: 2026-02-20 Tempo di lettura: 9 min

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 — flusso caldo per eventi una tantum: i nuovi abbonati non ricevono valori precedenti senza configurazione di replay.
  • MutableSharedFlow — versione mutabile con metodi emit() e tryEmit() per l'invio di eventi.
  • SharedFlow vs StateFlow: SharedFlow non conflaziona i valori (può bufferizzarne molti), non richiede un valore iniziale, adatto per eventi una tantum.
  • replay — numero di eventi recenti riprodotti ai nuovi abbonati (default 0).
  • extraBufferCapacity — buffer aggiuntivo per eventi oltre replay, prevenendo il blocco di emit().

Cos'è SharedFlow in Kotlin?

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.

SharedFlow vs StateFlow: quando usare cosa

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.

CriterioSharedFlowStateFlow
SemanticaEventi una tantum (navigazione, toast, avviso)Stato dell'UI (lista, caricamento, errore)
Valore inizialeNon richiestoRichiesto
Riproduzione all'abbonamentoSolo se replay > 0Sempre l'ultimo valore
ConflazioneNo — gli eventi non vengono persi (se il buffer non è pieno)Sì — memorizza solo l'ultimo
BufferizzazioneConfigurabile tramite replay + extraBufferCapacitySolo 1 (replay=1 fisso)
UtilizzonavigationEvent, showSnackbar, openDialogitems, 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: emit, tryEmit e bufferizzazione

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.

kotlin
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.

SharedFlow per eventi una tantum: il pattern Event

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.

kotlin
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.

Parametri di SharedFlow: replay, extraBufferCapacity, onBufferOverflow

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().

ParametroTipoDefaultDescrizione
replayInt0Numero di eventi recenti riprodotti a un nuovo abbonato. 0 = non riprodurre, 1 = come StateFlow
extraBufferCapacityInt0Buffer aggiuntivo oltre replay. Gli eventi sono memorizzati in un buffer circolare. 64 è il limite consigliato per la maggior parte degli scenari
onBufferOverflowBufferOverflowSUSPENDStrategia quando il buffer è pieno: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// 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.

Esempi di codice: SharedFlow in Kotlin

Esempio 1: SharedFlow per navigazione con Jetpack Navigation

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.

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

Esempio 2: SharedFlow con Room e operatori Flow

Uno scenario complesso: SharedFlow per notifiche di eventi in background combinato con StateFlow per l'UI.

kotlin
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

SharedFlow può perdere un evento?

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.

In cosa SharedFlow si differenzia da Channel?

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.

Come rendere SharedFlow thread-safe?

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.

Perché SharedFlow non viene usato per lo stato?

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.

Come testare SharedFlow?

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

  • SharedFlow — un flusso reattivo caldo per eventi una tantum non legati all'ultimo stato.
  • SharedFlow vs StateFlow: SharedFlow per eventi (navigazione, toast, avvisi), StateFlow per stato (liste, caricamento, errori).
  • MutableSharedFlow con replay=0, extraBufferCapacity=5, DROP_OLDEST è la configurazione standard per eventi UI.
  • emit() — funzione suspend per emissione bloccante; tryEmit() — non-suspend con risultato Boolean.
  • Il pattern UiEvent con sealed class è il metodo consigliato da Google per passare eventi una tantum da ViewModel a View.
  • SharedFlow garantisce la consegna a ogni abbonato, ma non garantisce la consegna di ogni evento in caso di overflow del buffer.
  • La combinazione di SharedFlow + StateFlow in un unico ViewModel è il pattern ottimale per un'architettura che separa stato ed eventi.

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.

Discuti il progetto

Leggi anche