SharedFlow es un flujo reactivo en caliente de la biblioteca Kotlin Coroutines, optimizado para eventos de una sola vez (one-shot events) que no deben repetirse al rotar la pantalla o al recrear el suscriptor. Mostramos en qué se diferencia SharedFlow de StateFlow: a diferencia de StateFlow, SharedFlow no almacena el último valor para nuevos suscriptores y admite la configuración de replay, extraBufferCapacity y onBufferOverflow. Según Google (Android Developers, 2025), SharedFlow es la solución recomendada para comandos de navegación, mensajes Snackbar y otros eventos que deben procesarse exactamente una vez.
Puntos Clave
SharedFlow es un flujo en caliente (hot flow) de la biblioteca kotlinx.coroutines.flow que, a diferencia de StateFlow, no está vinculado a un solo estado y puede emitir una cantidad arbitraria de eventos a suscriptores arbitrarios. SharedFlow es el tipo base para StateFlow — StateFlow se implementa a través de SharedFlow con replay = 1.
La característica clave de SharedFlow es que no tiene que almacenar el último valor. Por defecto (replay = 0), un nuevo suscriptor no recibe nada hasta que se envía un nuevo evento. Esto hace que SharedFlow sea ideal para escenarios donde un evento debe procesarse exactamente una vez: navegación, Snackbar, notificaciones del sistema, resultados de escaneo de código QR.
SharedFlow se estabilizó en kotlinx.coroutines 1.4.0 (noviembre de 2020) junto con StateFlow. Según la documentación de Kotlin Coroutines (2025), SharedFlow utiliza bloqueo de grano fino para la sincronización de suscriptores y proporciona escalabilidad lineal hasta 1000+ suscriptores concurrentes sin degradación del rendimiento, confirmado por pruebas de JetBrains.
La elección entre SharedFlow y StateFlow depende de la semántica de los datos transferidos: estado (StateFlow) o evento (SharedFlow). A continuación se presentan criterios claros con ejemplos.
| Criterio | SharedFlow | StateFlow |
|---|---|---|
| Semántica | Eventos de una sola vez (navegación, toast, alerta) | Estado de la UI (lista, carga, error) |
| Valor inicial | No requerido | Requerido |
| Reproducción al suscribirse | Solo si replay > 0 | Siempre el último valor |
| Conflación | No — los eventos no se pierden (si el búfer no está lleno) | Sí — almacena solo el último |
| Almacenamiento en búfer | Configurable mediante replay + extraBufferCapacity | Solo 1 (replay=1 fijo) |
| Uso | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
La regla más simple: si los datos deben mostrarse al rotar la pantalla — es estado (StateFlow). Si al rotar la pantalla el evento no debe repetirse — es un evento de una sola vez (SharedFlow). Por ejemplo, un toast con un mensaje de error es SharedFlow: al rotar, el toast no debe aparecer de nuevo. Una lista de productos es StateFlow: al rotar, la lista debe permanecer en pantalla.
En IT Sectr usamos SharedFlow para: comandos de navegación (transición de pantalla, apertura de enlace profundo), eventos de UI (Snackbar, AlertDialog), notificaciones del sistema (actualizaciones de datos en segundo plano, resultado de pago), eventos de analítica (registro, seguimiento).
MutableSharedFlow es la versión mutable de SharedFlow con los métodos emit() (suspend) y tryEmit() (no suspend) para enviar eventos. emit() se suspende si el búfer está lleno y onBufferOverflow = SUSPEND. tryEmit() devuelve un Boolean indicando si el evento se agregó correctamente al búfer.
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
}
Los parámetros del constructor son críticamente importantes: replay = 0 garantiza que el evento no se repita para un nuevo suscriptor; extraBufferCapacity = 10 proporciona un búfer para la emisión rápida de eventos antes de que la UI se suscriba; DROP_OLDEST es la estrategia de desbordamiento: los eventos antiguos se descartan, los nuevos se conservan. Según Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow con extraBufferCapacity = 64 procesa más de 100.000 eventos por segundo sin pérdidas.
Patrón Event (o UiEvent) es la forma recomendada por Google para pasar eventos de una sola vez desde ViewModel a la View. A diferencia del estado (StateFlow), un evento debe procesarse exactamente una vez, y al rotar la pantalla no debe repetirse. SharedFlow con replay = 0 es ideal para esta tarea.
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 ?: "Error de diseño"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
En la View (Activity/Fragment): la suscripción a eventos debe realizarse en lifecycleScope con repeatOnLifecycle(STATE.STARTED). En cada entrada a STARTED, la suscripción se recrea, pero el evento no se repite porque SharedFlow con replay=0 ya lo ha liberado. Esto garantiza que la navegación a la pantalla de seguimiento de pedidos ocurra solo una vez, no en cada rotación.
El constructor de MutableSharedFlow acepta tres parámetros que determinan el comportamiento del búfer. Una configuración incorrecta puede provocar pérdida de eventos o bloqueo de emit().
| Parámetro | Tipo | Default | Descripción |
|---|---|---|---|
| replay | Int | 0 | Número de eventos recientes reproducidos a un nuevo suscriptor. 0 = no reproducir, 1 = como StateFlow |
| extraBufferCapacity | Int | 0 | Búfer adicional más allá de replay. Los eventos se almacenan en un búfer circular. 64 es el límite recomendado para la mayoría de escenarios |
| onBufferOverflow | BufferOverflow | SUSPEND | Estrategia cuando el búfer está lleno: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Configuraciones para diferentes escenarios:
// 1. Eventos de UI de una sola vez (navegación, toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Flujo de replay para sincronización de estado (como StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Emisión de eventos de alta frecuencia (analítica, registros)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Importante: extraBufferCapacity + replay = tamaño total del búfer. Si emit() se llama más rápido de lo que el suscriptor procesa los eventos, el búfer se llena y se activa onBufferOverflow. Para eventos de UI, DROP_OLDEST es una estrategia segura: los eventos antiguos (navegaciones ya no relevantes) se descartan en favor de los nuevos. Para transacciones financieras, use SUSPEND — esto garantiza que ningún evento se pierda a costa de bloquear al emisor.
Los comandos de navegación son un caso de uso clásico para SharedFlow. Un Fragment se suscribe a los eventos y realiza la navegación. Al rotar la pantalla, el comando no se repite.
// 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
}
// En 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 escenario complejo: SharedFlow para notificaciones de eventos en segundo plano combinado con StateFlow para la 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("Nueva notificación: ${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("Todas las notificaciones marcadas como leídas")
}
}
}
En este ejemplo: StateFlow almacena la lista de notificaciones (estado — se conserva al rotar), SharedFlow emite mensajes toast (eventos de una sola vez — no se repiten al rotar). La combinación de dos tipos de Flow es el patrón recomendado por Google para ViewModel a partir de 2022.
Preguntas Frecuentes
Sí, si el búfer está lleno y onBufferOverflow = DROP_OLDEST o DROP_LATEST. SharedFlow no garantiza la entrega de cada evento — no es una cola de mensajes (como Channel). Si necesita entrega garantizada de todos los eventos, use Channel con búfer sin límite (UNLIMITED) o BroadcastChannel (obsoleto). Para eventos de UI, la pérdida de eventos obsoletos (por ejemplo, navegación antigua) es un comportamiento esperado, no un error.
Channel es una cola FIFO donde cada evento se entrega exactamente a un suscriptor (punto a punto). SharedFlow es una transmisión: cada evento se entrega a TODOS los suscriptores activos. SharedFlow se acerca más a BroadcastChannel (que está obsoleto) y es adecuado para escenarios de uno a muchos. Channel es para uno a uno (grupos de hilos, pipelines). Según la recomendación de JetBrains, SharedFlow es el reemplazo de BroadcastChannel en todos los proyectos nuevos.
SharedFlow ya es seguro para hilos — emit() y collect() están correctamente sincronizados. Varios hilos pueden llamar a emit() sin bloqueos, y todos los suscriptores activos reciben los eventos en el orden correcto. tryEmit() no es bloqueante — devuelve false si el búfer está lleno. Para sistemas de alta carga, use tryEmit() con DROP_OLDEST — esto evita el bloqueo de hilos.
SharedFlow sin replay=1 no almacena el último valor — al rotar la pantalla, un nuevo suscriptor no recibirá el estado actual y la UI permanecerá vacía. Con replay=1, SharedFlow se comporta como StateFlow pero pierde la optimización de comparación con equals(), lo que provoca notificaciones innecesarias cuando se emite el mismo valor nuevamente. StateFlow es la elección correcta para el estado; SharedFlow es para eventos.
Para probar SharedFlow, use Turbine — una biblioteca de Kotlin para probar Flow. Turbine permite verificar cada emisión individualmente con tiempos de espera y verificación de finalización. Ejemplo: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. También puede usar .toList() en runTest especificando la cantidad de eventos esperados.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también