Flow es un componente de la librería de corrutinas que nos permite implementar la programación reactiva.

Es el sustituto natural de RxJava, ya que la gran mayoría de las cosas que se pueden hacer las tenemos aquí, en general más sencillas, ya que se apoyan en conceptos que ya conocemos sobre corrutinas, secuencias y colecciones para darnos una solución muy fácil de comprender.

Qué son los Flows

Los Flows son secuencias asíncronas.

Los Flows son lazy

Al igual que las secuencias, los Flows son “lazy”, lo que quiere decir que hasta que alguien no requiere los valores del Flow, las operaciones que hay en ellos no se ejecutan.

Esto hace que se les llame flujos fríos (o cold streams), porque no van a empezar a proveer datos hasta que alguien pida recolectarlos. Además, si otro elemento se conecta al Flow, este empieza desde el primer valor del flujo.

Es importante entender esto, porque si un Flow realiza operaciones pesadas, estas se van a repetir cada vez que alguien recolecte sus valores.

Los Flows son asíncronos

A diferencia de las secuencias, que se procesan un elemento detrás de otro, en Flow no necesariamente pasa esto. Puede pasar un tiempo largo entre que nos llegue un valor y el siguiente.

Es por eso que normalmente no los ejecutaremos en el hilo principal, aunque sobre esto hablaremos luego.

Como todo esto ocurre en un contexto de corrutinas, piensa que el tema de la gestión de hilos va a ser muy fácil de gestionar.

Los Flows son secuenciales

Esto quiere decir que si un Flow va a generar x elementos, y estos consisten en un procesamiento pesado, se van a ejecutar uno detrás de otro: hasta que no acabe el anterior no empezará el siguiente.

Esto que muchas veces es una ventaja, en ocasiones puede ser un inconveniente. Imagina que tienes que hacer 10 peticiones a servidores y que cada una es independiente de la anterior. Aún así tendrías que esperar a que la anterior acabe para lanzar la siguiente.

Esto se puede modificar, y luego lo veremos. Pero quédate con que funcionan así por defecto.

Builders de Flows

Tenemos 3 formas de construir un Flow:

asFlow()

Quizá esta es la forma más sencilla de generar un Flow. Todos las colecciones, incluidas las secuencias, se pueden convertir en un Flow usando esta función

val flow = listOf(1, 2, 3, 4).asFlow()

flowOf()

Se genera un Flow con una secuencia de valores predefinidos, el equivalente a listOf() o sequenceOf()

val flow = flowOf(1, 2, 3, 4)

flow { }

El más versátil de todos, y el que seguramente usarás más a menudo. ¿Recuerdas cuando hablamos de las secuencias, que podíamos crear un bloque sequence { } y añadir valores con la función yield()?

Aquí es casi igual, solo que creamos un bloque flow { } y añadir valores con la función emit().

Además, aquí sumamos la ventaja de que este bloque recibe un contexto de corrutinas, por lo que podemos llamar a funciones suspend sin ningún problema dentro del mismo 😱

flow {
    for (i in (0..3)) {
        delay(200)
        emit(i)
    }
}

Tipos de operadores

Los Flows pueden transformarse igual que las colecciones, lo que los hace muy sencillos de usar.

Podemos filtrar, mapear, combinar, transformar… y un amplio número de operaciones que te permiten adaptar esos flujos a las necesidades que tengas en el lugar donde los utilizas.

Al igual que con las secuencias, hay dos tipos de operadoress

Operadores intermedios

Son operadores que no lanzan ninguna operación, independientemente de la complejidad que tengan. Lo que hacen es devolver un nuevo Flow que es la combinación del anterior con la nueva operación.

Podemos por ejemplo usar la operación filter():

makeFlow()
    .filter { it % 2 == 0 }

Y luego hacer un map() para transformar los resultados:

makeFlow()
    .filter { it % 2 == 0 }
    .map { "Value is $it" }

Pero hay un operador especialmente interesante, que es transform(), y que nos permite hacer transformaciones todo lo complejas que necesitemos. Lo único que tenemos que hacer es llamar a emit() con los valores que queramos devolver:

makeFlow()
    .transform { value ->
        emit(value)
        emit(value * value)
    }
}

También puedes combinar varios Flows con operaciones como zip() o combine():

val flow1 = flowOf(1, 2, 3, 4)
val flow2 = flowOf("1", "2", "3", "4")

flow1.zip(flow2) { a, b -> "$a -> $b" }

Recuerda que todos estos operadores pueden tener funciones de suspensión dentro, así que esas operaciones pueden ser tan simples como una suma o tan complejas como llamar a un servidor y guardar el resultado en una base de datos.

Operadores terminales

Estos sí que lanzan la ejecución y hacen que se comience la producción de valores y estos sean emitidos.

El operador terminal más habitual es collect(), que indica al Flow que ya hay alguien al otro lado (el recolector) esperando resultados, y que puede empezar a emitirlos.

makeFlow()
    .collect { print(it) }

Pero no solo hay este, tenemos varios más como toList(), toSet(), first(), single(), reduce() o fold().

Restricciones sobre contextos y excepciones

Flow tiene algunos restricciones sobre estos temas, para hacer que todo el sistema funcione correctamente.

El primero es que no podemos cambiar de contexto dentro del código de un flow. Si hacemos este, tendremos una excepción:

fun makeFlow() = flow {
    withContext(Dispatchers.IO) {
        for (i in (0..3)) {
            delay(200)
            emit(i)
        }
    }
}

Flow siempre va a ejecutarse en el contexto de la corrutina que lo lanzó. Pero muchas veces no será, lo que queremos, ¿cómo cambiamos entonces el contexto de ejecución? Podemos usar la función flowOn():

makeFlow()
    .flowOn(Dispatchers.IO)
    .collect { print(it) }

Con respecto a las excepciones, pasa un poco igual. No se debe capturar excepciones dentro de los flows, para no ocultarlas y que el resto del código no se entere. Hay una función especial para esto:

makeFlow()
    .catch { throwable -> println(throwable.message) }
    .collect { print(it) }
}

Si no capturamos la excepción, esta se seguirá propagando normalmente, como ocurre con el resto de componentes de corrutinas.

Conclusión

Esto solo ha sido una introducción de las cosas que podemos hacer con Flow, y los conceptos básicos que necesitas entender para empezar a aplicarlo.

En un siguiente artículo veremos algunos casos prácticos para que lo veas en un ejemplo más real.

Author: Antonio Leiva

Soy un apasionado de Kotlin. Hace ya más de dos años que estudio el lenguaje y su aplicación a Android para ayudarte a ti a aprenderlo de la forma más sencilla posible.