SharedFlow : qu'est-ce que c'est, SharedFlow vs StateFlow dans Android

Auteur : IT Sectr Publié le : 2026-02-20 Temps de lecture : 9 min

SharedFlow est un flux réactif chaud de la bibliothèque Kotlin Coroutines, optimisé pour les événements uniques (one-shot events) qui ne doivent pas se répéter lors de la rotation de l'écran ou de la recréation de l'abonné. Nous montrons en quoi SharedFlow diffère de StateFlow : contrairement à StateFlow, SharedFlow ne stocke pas la dernière valeur pour les nouveaux abonnés et prend en charge la configuration de replay, extraBufferCapacity et onBufferOverflow. Selon Google (Android Developers, 2025), SharedFlow est la solution recommandée pour les commandes de navigation, les messages Snackbar et autres événements qui doivent être traités exactement une fois.

Points Clés

  • SharedFlow — flux chaud pour événements uniques : les nouveaux abonnés ne reçoivent pas les valeurs précédentes sans configuration de replay.
  • MutableSharedFlow — version mutable avec méthodes emit() et tryEmit() pour envoyer des événements.
  • SharedFlow vs StateFlow : SharedFlow ne conflate pas les valeurs (peut en mettre plusieurs en mémoire tampon), ne nécessite pas de valeur initiale, adapté aux événements uniques.
  • replay — nombre d'événements récents rejoués aux nouveaux abonnés (par défaut 0).
  • extraBufferCapacity — tampon supplémentaire pour les événements au-delà de replay, empêchant le blocage de emit().

Qu'est-ce que SharedFlow en Kotlin ?

SharedFlow est un flux chaud (hot flow) de la bibliothèque kotlinx.coroutines.flow qui, contrairement à StateFlow, n'est pas lié à un seul état et peut émettre un nombre arbitraire d'événements à des abonnés arbitraires. SharedFlow est le type de base pour StateFlow — StateFlow est en fait implémenté via SharedFlow avec replay = 1.

La caractéristique clé de SharedFlow est qu'il n'a pas besoin de stocker la dernière valeur. Par défaut (replay = 0), un nouvel abonné ne reçoit rien jusqu'à ce qu'un nouvel événement soit envoyé. Cela rend SharedFlow idéal pour les scénarios où un événement doit être traité exactement une fois : navigation, Snackbar, notifications système, résultats de scan de code QR.

SharedFlow a été stabilisé dans kotlinx.coroutines 1.4.0 (novembre 2020) avec StateFlow. Selon la documentation Kotlin Coroutines (2025), SharedFlow utilise un verrouillage à grain fin pour la synchronisation des abonnés et offre une évolutivité linéaire jusqu'à plus de 1000 abonnés simultanés sans dégradation des performances, confirmé par les tests JetBrains.

SharedFlow vs StateFlow : quand utiliser quoi

Le choix entre SharedFlow et StateFlow dépend de la sémantique des données transférées : état (StateFlow) ou événement (SharedFlow). Vous trouverez ci-dessous des critères clairs avec des exemples.

CritèreSharedFlowStateFlow
SémantiqueÉvénements uniques (navigation, toast, alerte)État de l'UI (liste, chargement, erreur)
Valeur initialeNon requiseRequis
Rejeu à l'abonnementUniquement si replay > 0Toujours la dernière valeur
ConflationNon — les événements ne sont pas perdus (si le tampon n'est pas plein)Oui — stocke uniquement le dernier
Mise en tamponConfigurable via replay + extraBufferCapacitySeulement 1 (replay=1 fixe)
UtilisationnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

La règle la plus simple : si les données doivent être affichées lors de la rotation de l'écran — c'est un état (StateFlow). Si lors de la rotation de l'écran l'événement ne doit pas se répéter — c'est un événement unique (SharedFlow). Par exemple, un toast avec un message d'erreur est SharedFlow : à la rotation, le toast ne doit pas réapparaître. Une liste de produits est StateFlow : à la rotation, la liste doit rester à l'écran.

Chez IT Sectr, nous utilisons SharedFlow pour : les commandes de navigation (transition d'écran, ouverture de lien profond), les événements d'UI (Snackbar, AlertDialog), les notifications système (mises à jour de données en arrière-plan, résultat de paiement), les événements d'analyse (journalisation, suivi).

MutableSharedFlow : emit, tryEmit et mise en tampon

MutableSharedFlow est la version mutable de SharedFlow avec les méthodes emit() (suspend) et tryEmit() (non-suspend) pour envoyer des événements. emit() se suspend si le tampon est plein et onBufferOverflow = SUSPEND. tryEmit() retourne un Boolean indiquant si l'événement a été ajouté avec succès au tampon.

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
}

Les paramètres du constructeur sont critiques : replay = 0 garantit que l'événement ne se répète pas pour un nouvel abonné ; extraBufferCapacity = 10 fournit un tampon pour l'émission rapide d'événements avant que l'UI ne s'abonne ; DROP_OLDEST est la stratégie de débordement : les anciens événements sont supprimés, les nouveaux sont conservés. Selon Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow avec extraBufferCapacity = 64 traite plus de 100 000 événements par seconde sans perte.

SharedFlow pour événements uniques : le pattern Event

Pattern Event (ou UiEvent) est la méthode recommandée par Google pour transmettre des événements uniques de ViewModel à View. Contrairement à l'état (StateFlow), un événement doit être traité exactement une fois et, lors de la rotation de l'écran, il ne doit pas se répéter. SharedFlow avec replay = 0 est idéal pour cette tâche.

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 ?: "Erreur de mise en page"))
            }
        }
    }
}

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

Dans la View (Activity/Fragment) : l'abonnement aux événements doit être fait dans lifecycleScope avec repeatOnLifecycle(STATE.STARTED). À chaque entrée dans STARTED, l'abonnement est recréé, mais l'événement ne se répète pas car SharedFlow avec replay=0 l'a déjà libéré. Cela garantit que la navigation vers l'écran de suivi de commande se produit une seule fois, pas à chaque rotation.

Paramètres de SharedFlow : replay, extraBufferCapacity, onBufferOverflow

Le constructeur de MutableSharedFlow accepte trois paramètres qui déterminent le comportement du tampon. Une configuration incorrecte peut entraîner une perte d'événements ou un blocage de emit().

ParamètreTypeDéfautDescription
replayInt0Nombre d'événements récents rejoués à un nouvel abonné. 0 = ne pas rejouer, 1 = comme StateFlow
extraBufferCapacityInt0Tampon supplémentaire au-delà de replay. Les événements sont stockés dans un tampon circulaire. 64 est la limite recommandée pour la plupart des scénarios
onBufferOverflowBufferOverflowSUSPENDStratégie lorsque le tampon est plein : SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// Configurations pour différents scénarios :

// 1. Événements d'UI uniques (navigation, toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. Flux de rejeu pour synchronisation d'état (comme StateFlow)
val stateLike = MutableSharedFlow<AppState>(
    replay = 1,
    extraBufferCapacity = 0
)

// 3. Émission d'événements à haute fréquence (analytique, journaux)
val analytics = MutableSharedFlow<AnalyticsEvent>(
    replay = 0,
    extraBufferCapacity = 100,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

Important : extraBufferCapacity + replay = taille totale du tampon. Si emit() est appelée plus rapidement que l'abonné ne traite les événements, le tampon se remplit et onBufferOverflow se déclenche. Pour les événements d'UI, DROP_OLDEST est une stratégie sûre : les anciens événements (navigations obsolètes) sont supprimés au profit des nouveaux. Pour les transactions financières, utilisez SUSPEND — cela garantit qu'aucun événement n'est perdu au prix du blocage de l'émetteur.

Exemples de code : SharedFlow en Kotlin

Exemple 1 : SharedFlow pour la navigation avec Jetpack Navigation

Les commandes de navigation sont un cas d'utilisation classique pour SharedFlow. Un Fragment s'abonne aux événements et effectue la navigation. Lors de la rotation de l'écran, la commande ne se répète pas.

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
}

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

Exemple 2 : SharedFlow avec Room et opérateurs Flow

Un scénario complexe : SharedFlow pour les notifications d'événements en arrière-plan combiné avec StateFlow pour 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("Nouvelle notification : ${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("Toutes les notifications marquées comme lues")
        }
    }
}

Dans cet exemple : StateFlow stocke la liste des notifications (état — préservé à la rotation), SharedFlow émet des messages toast (événements uniques — non répétés à la rotation). La combinaison de deux types de Flow est le modèle recommandé par Google pour ViewModel à partir de 2022.

Questions Fréquentes

SharedFlow peut-il perdre un événement ?

Oui, si le tampon est plein et onBufferOverflow = DROP_OLDEST ou DROP_LATEST. SharedFlow ne garantit pas la livraison de chaque événement — ce n'est pas une file d'attente de messages (comme Channel). Si vous avez besoin d'une livraison garantie de tous les événements, utilisez Channel avec un tampon illimité (UNLIMITED) ou BroadcastChannel (obsolète). Pour les événements d'UI, la perte d'événements obsolètes (par exemple, une ancienne navigation) est un comportement attendu, pas un bug.

En quoi SharedFlow diffère-t-il de Channel ?

Channel est une file d'attente FIFO où chaque événement est livré à exactement un abonné (point à point). SharedFlow est une diffusion : chaque événement est livré à TOUS les abonnés actifs. SharedFlow est plus proche de BroadcastChannel (qui est obsolète) et convient aux scénarios un-à-plusieurs. Channel est pour un-à-un (pools de threads, pipelines). Selon la recommandation de JetBrains, SharedFlow est le remplacement de BroadcastChannel dans tous les nouveaux projets.

Comment rendre SharedFlow thread-safe ?

SharedFlow est déjà thread-safe — emit() et collect() sont correctement synchronisés. Plusieurs threads peuvent appeler emit() sans verrous, et tous les abonnés actifs reçoivent les événements dans le bon ordre. tryEmit() est non bloquant — il retourne false si le tampon est plein. Pour les systèmes à charge élevée, utilisez tryEmit() avec DROP_OLDEST — cela empêche le blocage des threads.

Pourquoi SharedFlow n'est-il pas utilisé pour l'état ?

SharedFlow sans replay=1 ne stocke pas la dernière valeur — lors de la rotation de l'écran, un nouvel abonné ne recevra pas l'état actuel et l'UI restera vide. Avec replay=1, SharedFlow se comporte comme StateFlow mais perd l'optimisation de comparaison via equals(), ce qui provoque des notifications inutiles lorsque la même valeur est émise à nouveau. StateFlow est le bon choix pour l'état ; SharedFlow est pour les événements.

Comment tester SharedFlow ?

Pour tester SharedFlow, utilisez Turbine — une bibliothèque Kotlin pour tester Flow. Turbine permet de vérifier chaque émission individuellement avec des délais d'attente et une vérification d'achèvement. Exemple : viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Vous pouvez également utiliser .toList() dans runTest en spécifiant le nombre d'événements attendus.

Résumé

  • SharedFlow — un flux réactif chaud pour les événements uniques non liés au dernier état.
  • SharedFlow vs StateFlow : SharedFlow pour les événements (navigation, toasts, alertes), StateFlow pour l'état (listes, chargement, erreurs).
  • MutableSharedFlow avec replay=0, extraBufferCapacity=5, DROP_OLDEST est la configuration standard pour les événements d'UI.
  • emit() — fonction suspend pour l'émission bloquante ; tryEmit() — non-suspend avec résultat Boolean.
  • Le pattern UiEvent avec sealed class est la méthode recommandée par Google pour transmettre les événements uniques de ViewModel à View.
  • SharedFlow garantit la livraison à chaque abonné, mais ne garantit pas la livraison de chaque événement en cas de débordement du tampon.
  • La combinaison de SharedFlow + StateFlow dans un seul ViewModel est le modèle optimal pour une architecture séparant état et événements.

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi