SharedFlow: qué es, SharedFlow vs StateFlow en Android

Autor: IT Sectr Publicado: 2026-02-20 Tiempo de lectura: 9 min

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 — flujo en caliente para eventos de una sola vez: los nuevos suscriptores no reciben valores anteriores sin configuración de replay.
  • MutableSharedFlow — versión mutable con métodos emit() y tryEmit() para enviar eventos.
  • SharedFlow vs StateFlow: SharedFlow no conflaciona valores (puede almacenar varios en búfer), no requiere un valor inicial, adecuado para eventos de una sola vez.
  • replay — cantidad de eventos recientes reproducidos a nuevos suscriptores (por defecto 0).
  • extraBufferCapacity — búfer adicional para eventos más allá de replay, evitando el bloqueo de emit().

¿Qué es SharedFlow en Kotlin?

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.

SharedFlow vs StateFlow: cuándo usar cada uno

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.

CriterioSharedFlowStateFlow
SemánticaEventos de una sola vez (navegación, toast, alerta)Estado de la UI (lista, carga, error)
Valor inicialNo requeridoRequerido
Reproducción al suscribirseSolo si replay > 0Siempre el último valor
ConflaciónNo — los eventos no se pierden (si el búfer no está lleno)Sí — almacena solo el último
Almacenamiento en búferConfigurable mediante replay + extraBufferCapacitySolo 1 (replay=1 fijo)
UsonavigationEvent, showSnackbar, openDialogitems, 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: emit, tryEmit y almacenamiento en búfer

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.

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
}

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.

SharedFlow para eventos de una sola vez: el patrón Event

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.

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 ?: "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.

Parámetros de SharedFlow: replay, extraBufferCapacity, onBufferOverflow

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ámetroTipoDefaultDescripción
replayInt0Número de eventos recientes reproducidos a un nuevo suscriptor. 0 = no reproducir, 1 = como StateFlow
extraBufferCapacityInt0Bú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
onBufferOverflowBufferOverflowSUSPENDEstrategia cuando el búfer está lleno: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// 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.

Ejemplos de código: SharedFlow en Kotlin

Ejemplo 1: SharedFlow para navegación con Jetpack Navigation

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.

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
}

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

Ejemplo 2: SharedFlow con Room y operadores Flow

Un escenario complejo: SharedFlow para notificaciones de eventos en segundo plano combinado con StateFlow para la 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("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

¿Puede SharedFlow perder un evento?

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.

¿En qué se diferencia SharedFlow de Channel?

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.

¿Cómo hacer que SharedFlow sea seguro para hilos?

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.

¿Por qué no se usa SharedFlow para el estado?

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.

¿Cómo probar SharedFlow?

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

  • SharedFlow — un flujo reactivo en caliente para eventos de una sola vez no vinculados al último estado.
  • SharedFlow vs StateFlow: SharedFlow para eventos (navegación, toasts, alertas), StateFlow para estado (listas, carga, errores).
  • MutableSharedFlow con replay=0, extraBufferCapacity=5, DROP_OLDEST es la configuración estándar para eventos de UI.
  • emit() — función suspend para emisión bloqueante; tryEmit() — no suspend con resultado Boolean.
  • El patrón UiEvent con sealed class es la forma recomendada por Google para pasar eventos de una sola vez desde ViewModel a la View.
  • SharedFlow garantiza la entrega a cada suscriptor, pero no garantiza la entrega de cada evento cuando el búfer se desborda.
  • La combinación de SharedFlow + StateFlow en una sola ViewModel es el patrón óptimo para una arquitectura que separa estado y eventos.

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.

Discutir el proyecto

Lea también