Existen varios tipos de Flows muy particulares que nos van a solucionar la vida cuando tengamos que hacer cosas muy concretas.

Ya vimos StateFlow en un artículo anterior, y en esta ocasión hablamos de CallbackFlow

¿Qué es CallbackFlow?

Es un tipo de Flow que nos permite convertir cualquier API basada en callbacks (o en Listeners, que en Android pasa mucho) en un Flow que podemos recolectar, transformar y usar como hemos visto en el resto de artículos sobre este tema.

Esto va a hacer que podamos convertir cualquier API al mundo reactivo.

En Android, por ejemplo, podríamos convertir cualquier Listener de los componentes de la interfaz de usuario en un Flow, de la misma forma que por ejemplo la librería RxBinding lo hace con RxJava.

Vamos a hacer algunos ejemplos:

Convirtiendo setOnClickListener a un Flow

Este es el ejemplo más clásico. Tenemos el típico setOnClickListener, y queremos emitir eventos cada vez que se haga click en el botón.

Lo primero sería crearnos una función de extensión que nos devuelva ese Flow

val View.onClickEvents: Flow<View>
    get() = ...

Y aquí es donde creamos nuestro CallbackFlow. se usa una función para ello, un bloque de código al estilo de flow { }:

val View.onClickEvents: Flow<View>
    get() = callbackFlow {
        ...
    }

Después vamos a crearnos un listener que extienda de View.OnClickListener y asignárselo a la vista:

val View.onClickEvents: Flow<View>
    get() = callbackFlow {
        val onClickListener = View.OnClickListener { ... }
        setOnClickListener(onClickListener)
    }

En el listener, lo que haremos será llamar a la función offer(), que es la que ofrece un nuevo valor al Flow para emitirlo:

val View.onClickEvents: Flow<View>
    get() = callbackFlow {
        val onClickListener = View.OnClickListener { offer(it) }
        setOnClickListener(onClickListener)
    }

Casi para terminar, le vamos a decir que cuando el Flow se cierre, nos desuscribamos del listener. Para ello::

val View.onClickEvents: Flow<View>
    get() = callbackFlow {
        val onClickListener = View.OnClickListener { offer(it) }
        setOnClickListener(onClickListener)
        awaitClose { setOnClickListener(null) }
    }.

Para terminar, lo ideal en las interfaces de usuario, en las que solo nos interesa el valor final, y no toda la lista de valores intermedios, es que usemos la función conflate().

Con esto, si el recolector tarda más de lo esperado en ejecutar la operación con el resultado, esto no va a suspender al Flow hasta que acabe. El Flow seguirá emitiendo valores, y cuando el recolector acabe con el que estaba, cogerá el último emitido en ese momento.

val View.onClickEvents: Flow<View>
    get() = callbackFlow {
        val onClickListener = View.OnClickListener { offer(it) }
        setOnClickListener(onClickListener)
        awaitClose { setOnClickListener(null) }
    }.conflate()

Convirtiendo el Scroll Listener a un Flow

En artículos anteriores, nos suscribimos al evento de scroll del RecyclerView de la siguiente forma:

val layoutManager = recycler.layoutManager as GridLayoutManager

recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        viewModel.lastVisible.value = layoutManager.findLastVisibleItemPosition()
    }
})

Con ellos, podíamos actualizar el StateFlow basados en la última posición visible del LayoutManager. ¿Por qué no acortamos todo esto, y nos creamos un Flow que devuelva las actualizaciones de la última posición visible?

La estructura sería esta:

val RecyclerView.lastVisibleEvents: Flow<Int>
    get() = callbackFlow<Int> {
        ...
    }.conflate()

Vamos a recuperar el LayoutManager y crear el OnScrollListener:

val RecyclerView.lastVisibleEvents: Flow<Int>
    get() = callbackFlow<Int> {
        val lm = layoutManager as GridLayoutManager

        val listener = object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                ...
            }
        }
        addOnScrollListener(listener)
    }.conflate()

Ya solo nos quedaría ofrecer la última posición visible:

val RecyclerView.lastVisibleEvents: Flow<Int>
    get() = callbackFlow<Int> {
        val lm = layoutManager as GridLayoutManager

        val listener = object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                offer(lm.findLastVisibleItemPosition())
            }
        }
        awaitClose { removeOnScrollListener(listener) }
    }.conflate()

Y por supuesto, desuscribirnos del Listener cuando se cierre el Flow:

val RecyclerView.lastVisibleEvents: Flow<Int>
    get() = callbackFlow<Int> {
        val lm = layoutManager as GridLayoutManager

        val listener = object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                offer(lm.findLastVisibleItemPosition())
            }
        }
        addOnScrollListener(listener)
        awaitClose { removeOnScrollListener(listener) }
    }.conflate()

Conclusión

Ya ves que es muy sencillo adaptar cualquier cosa a un Flow.

Lo bueno es que a partir de ese punto, puedes combinarlos con otros, realizar transformaciones sobre ellos y realizar todo tipo de operaciones para que se adapten a tus necesidades.

Los Flows son una herramienta muy potente que nos mueve de forma muy sencilla la mundo de la programación reactiva en Kotlin.

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.