Si te dedicas al desarrollo de Android, ya sabrás que gestionar cómo los datos llegan a la pantalla puede volverse un auténtico dolor de cabeza. Durante mucho tiempo, LiveData fue el rey indiscutible, pero la llegada de las Corrutinas de Kotlin y los Flows ha cambiado las reglas del juego, ofreciendo una potencia y flexibilidad que antes eran impensables.
En el ecosistema actual, no basta con que la app funcione; necesitamos que sea eficiente en el consumo de recursos y que no se rompa al girar la pantalla. Aquí es donde entran en escena StateFlow y SharedFlow, dos herramientas que nos permiten manejar flujos de datos «calientes», asegurando que la interfaz de usuario esté siempre sincronizada con la lógica de negocio.
Desmontando el concepto de StateFlow
Podemos imaginar que StateFlow es un contenedor de estados que siempre tiene un valor actual. A diferencia de los flujos estándar, que son «fríos» y solo actúan cuando alguien los escucha, StateFlow es un flujo caliente, lo que significa que existe y mantiene su valor en memoria independientemente de si hay una vista observándolo en ese momento.
Cuando un nuevo componente de la UI comienza a recolectar este flujo, recibe inmediatamente el último estado emitido. Esto lo hace ideal para representar el estado de una pantalla, como si una lista de noticias está cargando, si se muestra un error o si los datos ya están listos para ser pintados. Para modificar este estado, solemos utilizar MutableStateFlow dentro de un ViewModel, exponiéndolo como un StateFlow inmutable hacia la vista para evitar que cualquier clase externa altere el estado.
Un punto clave es su comportamiento de conflación basada en la igualdad. Si intentas emitir un valor que es idéntico al anterior, StateFlow simplemente lo ignora. Esto evita que la interfaz de usuario realice recomposiciones innecesarias, optimizando así el rendimiento general de la aplicación y mejorando la experiencia del usuario final.
La versatilidad de SharedFlow para eventos efímeros
A diferencia del anterior, SharedFlow no está diseñado para retener un estado, sino para emitir eventos. Es como una emisora de radio: lanza la señal y, si hay alguien escuchando, recibe la información; si no, la señal se pierde en el vacío. Esto es perfecto para lo que llamamos eventos de un solo disparo, como mostrar un Toast, lanzar un diálogo de confirmación o ejecutar una navegación hacia otra pantalla.
Una de sus mayores ventajas es que es altamente configurable. Podemos definir el replay cache, que determina cuántos valores previos recibirá un nuevo suscriptor, o ajustar la política de onBufferOverflow. Si el búfer se llena, podemos elegir entre suspender al emisor, descartar el elemento más antiguo o el más reciente, dándonos un control total sobre la presión de los datos.
En arquitecturas modernas, SharedFlow se utiliza para implementar la comunicación de efectos en el patrón UDF (Unidirectional Data Flow). Mientras que el estado «baja» hacia la UI a través de StateFlow, los efectos laterales se gestionan con SharedFlow, permitiendo que el ViewModel notifique a la vista que debe realizar una acción puntual sin alterar el estado global de la pantalla.
Implementación técnica y seguridad en la UI
Para que todo esto funcione sin drenar la batería ni causar crashes, es vital cómo recolectamos estos flujos. Usar launch o launchIn directamente en la UI es un error común que puede provocar fallos cuando la vista no es visible. La solución profesional es emplear repeatOnLifecycle, que asegura que la recolección solo ocurra cuando la app esté en el estado STARTED o superior, deteniéndose automáticamente al pasar a STOPPED.
Bajo el capó, estas herramientas utilizan un sistema de asignación de slots para rastrear a los recolectores activos. Esta arquitectura interna optimiza la memoria reutilizando espacios y evita la creación constante de objetos. Además, StateFlow emplea una técnica de combinación plana (flat combining) que permite manejar actualizaciones concurrentes de forma eficiente sin bloquear excesivamente el hilo de ejecución.
Si tenemos un flujo frío (como una petición de base de datos con Room) y queremos convertirlo en uno caliente, podemos usar el operador stateIn o shareIn. Estos operadores requieren un CoroutineScope y una política de inicio, como WhileSubscribed, que mantiene el flujo activo solo mientras haya suscriptores, evitando así que el procesador trabaje en vano cuando el usuario no está viendo la pantalla.
La transición de LiveData a StateFlow es sencilla pero potente. Mientras que LiveData requiere que la vista esté activa para observar, StateFlow se integra nativamente con las corrutinas, permitiendo operaciones asíncronas complejas y una gestión de errores mucho más transparente. Al final del día, la elección entre uno u otro depende de si necesitamos recordar la última actualización o si simplemente queremos avisar que algo ha ocurrido.
Dominar la combinación de StateFlow para la persistencia del estado y SharedFlow para la gestión de eventos permite construir aplicaciones robustas donde la lógica de negocio está totalmente separada de la presentación, garantizando que la interfaz sea reactiva, predecible y capaz de soportar cualquier cambio de configuración sin perder la información del usuario.