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 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.
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ère | SharedFlow | StateFlow |
|---|---|---|
| Sémantique | Événements uniques (navigation, toast, alerte) | État de l'UI (liste, chargement, erreur) |
| Valeur initiale | Non requise | Requis |
| Rejeu à l'abonnement | Uniquement si replay > 0 | Toujours la dernière valeur |
| Conflation | Non — les événements ne sont pas perdus (si le tampon n'est pas plein) | Oui — stocke uniquement le dernier |
| Mise en tampon | Configurable via replay + extraBufferCapacity | Seulement 1 (replay=1 fixe) |
| Utilisation | navigationEvent, showSnackbar, openDialog | items, 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 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.
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.
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.
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.
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ètre | Type | Défaut | Description |
|---|---|---|---|
| replay | Int | 0 | Nombre d'événements récents rejoués à un nouvel abonné. 0 = ne pas rejouer, 1 = comme StateFlow |
| extraBufferCapacity | Int | 0 | Tampon 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 |
| onBufferOverflow | BufferOverflow | SUSPEND | Stratégie lorsque le tampon est plein : SUSPEND, DROP_OLDEST, DROP_LATEST |
// 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.
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.
// 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()
}
}
}
}
Un scénario complexe : SharedFlow pour les notifications d'événements en arrière-plan combiné avec StateFlow pour 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("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
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.
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.
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.
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.
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é
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.
Lisez aussi