Convertir cualquier callback en un Flow con CallbackFlow
Antonio Leiva

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.

Quizá también te interese…

Kotlin 1.5.0 : Las 5 novedades que puedes empezar a usar hoy

Kotlin 1.5.0 : Las 5 novedades que puedes empezar a usar hoy

Kotlin 1.5.0 ya está aquí, y como siempre trae una serie de novedades que te van a interesar muchísimo. Cabe destacar que a partir de ahora, de acuerdo las nuevas versiones de Kotlin se lanzarán cada 6 meses, independientemente de las nuevas funcionalidades que...

¿Qué es Kotlin y para qué sirve?

¿Qué es Kotlin y para qué sirve?

Kotlin es un lenguaje de programación de código abierto creado por JetBrains que se ha popularizado gracias a que se puede utilizar para programar aplicaciones Android. Pero si has llegado hasta aquí pensando que Kotlin solo se puede usar en Android, lo que te voy a...

2 formas de recolectar Flows en la UI que SÍ funcionan

2 formas de recolectar Flows en la UI que SÍ funcionan

En la serie de artículos sobre Programación Reactiva con Flow hemos visto muchos conceptos, y hemos aprendido cómo aplicarlos al desarrollo Android. Pero hay algo que no hemos hecho del todo bien. Esto es la recolección de Flows desde la Activity (o el Fragment, en...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *