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…

Varianza en Kotlin – Gana la batalla a los genéricos

Varianza en Kotlin – Gana la batalla a los genéricos

Cuando hablamos de herencia, normalmente tenemos claro que cuando una clase hereda de otra, podemos usar la clase más genérica para dar soluciones más flexibles. Por tanto, si tengo los tipos Int y Long, puedo crearme una función que reciba Number y dar una solución...

Contracts en Kotlin: Haz más listo al compilador

Contracts en Kotlin: Haz más listo al compilador

El compilador de Kotlin es muy potente, y nos puede ayudar en muchos aspectos en los que otros compiladores como Java pasan de largo. Temas como los nulos, inferencia de tipos, genéricos, smart casting, y un largo etcétera, hacen del compilador de Kotlin una...

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...

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.

Acepto la política de privacidad *