Cuando nos metemos en el mundo de las corrutinas en Android, nos encontramos con que a veces una sola respuesta no es suficiente. Aquí es donde entran los Kotlin Flows, que básicamente son herramientas capaces de soltar una ristra de valores uno tras otro, a diferencia de las funciones de suspensión tradicionales que solo nos devuelven un dato y ya está. Imagínate que necesitas actualizaciones constantes de una base de datos; un flujo es el compañero ideal para este trabajo.
Para que nos entendamos, un Flow es como una tubería de datos que se calcula de forma asíncrona. Es muy parecido a un Iterator, pero con la ventaja de que usa funciones de suspensión para no bloquear el hilo principal mientras se producen los valores. Esto es vital para que la aplicación no se quede colgada mientras esperamos que llegue una respuesta de la red o se procese un archivo pesado.
Los protagonistas del flujo de datos
En cualquier sistema de transmisión de datos tenemos tres figuras clave. Primero está el productor, que es quien genera la información y la lanza al flujo. Luego tenemos a los intermediarios, que son opcionales y se encargan de retocar o filtrar los datos antes de que lleguen al final. Por último, el consumidor es quien recoge esos valores para hacer algo con ellos, como refrescar la pantalla del usuario.
En el día a día de Android, solemos ver que el repositorio actúa como productor, mientras que la interfaz de usuario (IU) hace de consumidor final. A veces ocurre al revés, donde la IU produce eventos que otras capas deben procesar. Las capas intermedias son las que ajustan la información para que encaje exactamente con lo que la siguiente capa necesita.
Cómo poner en marcha un flujo
Para crear uno desde cero, lo más habitual es usar la función
flow. Dentro de este bloque, podemos usar la función emit para enviar los datos manualmente. Por ejemplo, si tenemos una fuente de noticias que debe actualizarse cada ciertos segundos, podemos meter un bucle infinito con un delay para que el flujo siga vivo y enviando datos frescos.
Eso sí, hay un par de reglas que no podemos saltarnos. Primero, los flujos son estrictamente secuenciales; si llamas a una función de suspensión, el productor se detendrá hasta que esta termine. Segundo, no puedes llamar a
emit desde un CoroutineContext diferente al del productor. Si necesitas cambiar el contexto, no intentes crear corrutinas nuevas dentro del bloque flow, mejor recurre a callbackFlow.Transformando y consumiendo la información
Si queremos modificar los datos sin consumirlos todavía, usamos los operadores intermedios. Estos operadores, como
map o onEach, crean una cadena de procesos que se quedan «dormidos» hasta que alguien realmente pida los datos. Es una forma muy eficiente de transformar la información, por ejemplo, filtrando solo las noticias que el usuario ha marcado como favoritas antes de mandarlas a la vista.Para activar todo este mecanismo, necesitamos un operador terminal. El más común es
collect, que es una función de suspensión y, por tanto, debe vivir dentro de una corrutina. Cuando ejecutamos collect, el productor se pone en marcha y empieza a emitir. El flujo se cerrará cuando la corrutina se cancele (como ocurre al borrar un ViewModel) o cuando el productor termine de emitir todos sus elementos.Gestión de errores y contextos de ejecución
Como no todo es color de rosa y las librerías externas pueden fallar, contamos con el operador
catch. Este nos permite atrapar excepciones inesperadas y decidir qué hacer: podemos simplemente avisar al usuario del error o incluso emitir valores de caché para que la aplicación no se quede vacía mientras no hay conexión.Otro punto crítico es dónde se ejecuta el código. Por defecto, el productor usa el contexto de quien hace el
collect. Si queremos que el trabajo pesado de E/S no sature el hilo principal, utilizamos flowOn. Este operador cambia el contexto del flujo ascendente, permitiendo que el productor y los operadores previos se ejecuten en un despacho optimizado como Dispatchers.IO, mientras que el consumidor sigue en el hilo de la IU.Casos avanzados: callbackFlow y debounce
A veces nos topamos con APIs antiguas que usan callbacks en lugar de corrutinas. Para esto existe
callbackFlow, que nos permite convertir esas devoluciones de llamada en flujos. A diferencia del flow normal, aquí podemos usar trySend para enviar datos desde contextos diferentes. Es fundamental usar awaitClose para limpiar las suscripciones y evitar fugas de memoria.Un uso muy práctico de esto es implementar búsquedas en tiempo real en un
EditText. Mediante la función debounce, podemos evitar que la aplicación haga una petición al servidor por cada letra que el usuario escribe. Si configuramos un margen de 500 milisegundos, el flujo solo emitirá la consulta cuando el usuario haya dejado de escribir brevemente, optimizando así el rendimiento y el consumo de datos.Composición de múltiples flujos
Cuando la app crece, necesitamos combinar varias fuentes de datos. El operador
zip empareja valores estrictamente uno a uno; si un flujo es más lento, el otro espera. Por otro lado, combine es más dinámico: emite un resultado cada vez que cualquiera de los flujos cambia, usando siempre el último valor conocido de los demás. Es ideal para pantallas que dependen de múltiples estados en tiempo real.Si lo que queremos es simplemente juntar varios flujos en uno solo sin combinarlos, usamos
merge. Este operador reenvía los valores tal cual llegan, manteniendo el orden de emisión de cada fuente. Es la opción perfecta para gestionar eventos independientes, como clics de botones y gestos de pantalla, en un único canal de procesamiento.Para que todo este sistema sea robusto, lo ideal es manejar los errores lo más cerca posible de la fuente, usar clases selladas (sealed classes) o el tipo
Result para representar estados de éxito o fallo, y aplicar estrategias de reintento con retry para fallos temporales. También es recomendable usar buffer o conflate si el productor es mucho más rápido que el consumidor para evitar cuellos de botella.La capacidad de gestionar secuencias asíncronas mediante flujos permite crear aplicaciones Android mucho más fluidas, aprovechando la potencia de las corrutinas para transformar, combinar y filtrar datos en tiempo real sin comprometer la estabilidad del hilo principal ni la experiencia del usuario.
Continúar leyendo...