Si te estás metiendo en el mundo de la programación reactiva con Kotlin, seguro que te has topado con que no todos los flujos se comportan igual. Para los que no están tan puestos, básicamente hablamos de si el flujo es un «estático» que espera a que alguien lo llame o si es un «dinámico» que está ahí bombeando datos aunque nadie lo esté mirando en ese momento. Manejar esto bien es la diferencia entre una app que vuela y una que se come la memoria del dispositivo.
En este artículo vamos a destripar a fondo cómo pasar de esos flujos fríos, que son los que vienen por defecto, a los flujos calientes usando stateIn y sharedIn. No nos vamos a quedar solo en la teoría, sino que vamos a ver cómo implementarlo en ViewModel y, lo más importante, cómo evitar que las pruebas unitarias se vuelvan una pesadilla cuando los datos no se emiten como esperábamos.
Entendiendo la diferencia entre flujos fríos y calientes
Para empezar con buen pie, hay que entender que un Cold Flow es como una canción en Spotify que empieza desde el segundo cero cada vez que le das al play; es decir, cada recolector recibe su propia secuencia de datos de forma independiente. Son ideales para tareas que consumen muchos recursos y que solo deben ejecutarse cuando hay alguien escuchando, como una consulta a la base de datos.
Por otro lado, los Hot Flows son más como una emisora de radio: la música suena independientemente de si tienes la radio encendida o no. Aquí, múltiples suscriptores comparten el mismo flujo de datos. Estos son fundamentales cuando necesitas gestionar el estado de la interfaz de usuario o disparar eventos que deben llegar a varias partes de la aplicación simultáneamente.
StateFlow: El guardián del estado
El StateFlow es un tipo de flujo caliente especializado en mantener un estado. Su característica principal es que siempre retiene el valor más reciente, lo que lo convierte en la herramienta perfecta para sustituir al antiguo LiveData en Android. Para que funcione, requiere un valor inicial en su constructor, asegurando que la UI siempre tenga algo que mostrar.
Cuando trabajamos en un ViewModel, solemos usar un MutableStateFlow privado para poder cambiar el valor internamente y exponemos un StateFlow público para que la vista solo pueda leerlo. Es vital recordar que StateFlow combina las emisiones; si los valores cambian demasiado rápido, el recolector podría saltarse algunos estados intermedios y recibir solo el último, lo cual es eficiente para la interfaz de usuario.
SharedFlow: Emisor de eventos globales
A diferencia del anterior, el SharedFlow no guarda un estado actual por defecto, sino que se usa para transmitir eventos a múltiples suscriptores. Imagina que necesitas avisar a toda la app de que el usuario ha cerrado sesión; un SharedFlow es la opción ideal porque permite configurar el replay, que define cuántos valores antiguos se envían a los nuevos suscriptores.
Además, ofrece un control exhaustivo sobre la contrapresión mediante el parámetro onBufferOverflow. Puedes decidir si el emisor debe suspenderse cuando el búfer esté lleno, o si prefieres descartar el elemento más antiguo o el más reciente para no bloquear la ejecución del programa.
Transformando flujos fríos con stateIn y sharedIn
A veces tenemos un flujo frío que es carísimo de mantener (como una conexión de red persistente) y no queremos que cada suscriptor abra una conexión nueva. Aquí es donde entra el operador stateIn, que convierte un flujo frío en un StateFlow caliente compartido. Este operador necesita un CoroutineScope y una estrategia de inicio.
La configuración de SharingStarted es clave: Lazily comienza la recolección con el primer suscriptor y no se detiene, mientras que WhileSubscribed es la opción más inteligente para Android, ya que detiene la producción de datos cuando ya no hay nadie escuchando (por ejemplo, cuando la app está en segundo plano), ahorrando batería y memoria.
Si lo que necesitas es un SharedFlow en lugar de un StateFlow, el operador a usar es shareIn. Este funciona de forma similar pero es más flexible, permitiéndote definir exactamente cuántos elementos se deben retransmitir a los nuevos integrantes del flujo mediante el parámetro replay.
Estrategias de Testing para Flujos Calientes
Probar estos flujos tiene su miga. Cuando el sujeto de la prueba es el que observa el flujo, lo mejor es crear implementaciones falsas (fakes) de los repositorios que emitan valores controlados. Si el módulo expone el flujo, podemos usar funciones como first() para validar la primera emisión o toList() para flujos finitos.
Un problema común con stateIn es que, si usamos WhileSubscribed, la prueba puede fallar porque el flujo no se activa si no hay un recolector. Para solucionar esto, debemos lanzar un recolector vacío en el backgroundScope de la prueba, asegurando que el operador se active y actualice los valores. Para hacer el proceso más sencillo, se recomienda el uso de la librería Turbine, que permite validar las emisiones de forma secuencial y mucho más natural.
- Para StateFlows, la recomendación es validar directamente la propiedad value en lugar de recolectar el flujo, evitando así problemas con la combinación de valores rápidos.
- Es fundamental usar UnconfinedTestDispatcher para garantizar que la corrutina de recolección esté lista antes de que se emitan los datos en la prueba.
Dominar la transición entre flujos fríos y calientes mediante la correcta aplicación de stateIn y sharedIn permite construir arquitecturas en Android mucho más robustas, optimizando el consumo de recursos y asegurando que la sincronización de datos entre la lógica de negocio y la interfaz de usuario sea impecable y fácil de testear. Comparte esta información para que más personas conozcan del tema.