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

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 caso de que los uses).

Hemos llegado hasta donde las herramientas de Android hoy en días nos permiten, pero esto tiene un pequeño problema.

Veamos este caso:

lifecycleScope.launchWhenStarted {
    viewModel.spinner.collect { visible ->
        progress.visible = visible
    }
}

Esto está muy bien, porque nos estamos asegurando de que cuando la activity esté en el estado onStop, la interfaz no intente actualizarse con los resultados que se recolectan.

Pero seguimos teniendo un problema. Imagina que estamos recolectando datos de un Flow que cada x segundos (o basado en ciertas reglas) sigue haciendo peticiones a un servidor, siempre y cuando haya alguien recolectando.

En este caso, el Flow no va a dejar de trabajar, y por tanto estamos haciendo peticiones innecesarias. Sí, nuestra App no explota por intentar actualizarla en segundo plano, pero tenemos una App haciendo cosas que no hacen falta.

¿Cómo podemos mejorar esto? Hay 2 opciones

Opción 1: Crear un Scope propio de corrutinas

Todas estas cosas que nos ahorran código están muy bien, pero nos hacen perder control sobre lo que está ocurriendo.

Si somos explícitos sobre cuándo creamos un scope y cuándo lo cancelamos, entonces sabemos exactamente qué está pasando en cada momento.

Si no sabes sobre scopes, aquí tienes una pequeña explicación y en este otro artículo te lo explico más a fondo.

La forma más sencilla de hacerlo es que la Activity implemente CoroutineScope. Para ello. Esto nos obliga también a tener un CoroutineContext:

class MainActivity : AppCompatActivity(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = TODO("Not yet implemented")
}

Ese CoroutineScope, como ya explicaba en los otros recursos que te enlacé, requiere de un Job y un Dispatcher:

private lateinit var job: Job

override val coroutineContext: CoroutineContext
   get() = Dispatchers.Main + job

Este contexto va a ejecutar siempre por defecto su código en el Main dispatcher, y va a usar ese Job padre que controlará todas las corrutinas asociadas que se ejecuten.

¿Qué tenemos que hacer ahora? Simplemente crear y cancelar ese Job cuando lo necesitemos.

Si lo que queremos es que todo empiece en el onStart y acabe en el onStop, solo necesitamos este código:

override fun onStart() {
    super.onStart()
    job = SupervisorJob()
}

override fun onStop() {
    job.cancel()
    super.onStop()
}

Creamos el Job en el onStart y lo cancelamos en el onStop. Ya está, problema resuelto.

Es verdad que es un poco más de código, pero bien encapsulado no debería ser mayor problema.

Y ahora es muy sencillo empezar a recolectar un Flow:

launch {
    viewModel.spinner.collect { progress.visible = it }
}

Ahora, el equipo de Android siempre está tratando de evitarnos escribir código repetitivo, así que nos dan otra opción.

Opción 2: LifecycleOwner.addRepeatingJob

En las nuevas alphas de la librería lifecycle-runtime-ktx se ha incluido esta nueva función, que puede hacer lo mismo sin necesidad de crear el scope manualmente.

La función addRepeatingJob recibe como parámetro el momento en el que esa tarea debe empezar a ejecutarse (el job se crea), y por tanto lo cancelará en el estado opuesto.

Para ello, necesitamos añadir esta versión de la librería o superiores:

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01'

Ahora, la única modificación que necesitaríamos en nuestro código original sería sustituir la llamada a launchWhenStarted con esta nueva función:

addRepeatingJob(Lifecycle.State.STARTED) {
    viewModel.spinner.collect { progress.visible = it }
}

Como ves, muy sencillo y además muy versátil, porque puedes definir distintos jobs que sobrevivan a distintos estados de la Activity o el Fragment.

Esto también nos da la opción de usar un equivalente a flowOn que ya vimos en el artículo de los conceptos básicos sobre Flows:

viewModel.movies.flowWithLifecycle(
    lifecycle,
    Lifecycle.State.STARTED
)

Como nota curiosa, aquí el estado STARTED es el valor por defecto, así que no haría falta ni ponerlo (en addRepeatingJob sí!):

viewModel.movies.flowWithLifecycle(lifecycle)

Google está a tope de Flow

Pues sí, está claro que la programación reactiva ha venido para quedarse (no hay más que ver la incursión de Jetpack Compose en nuestras vidas, o los ViewModels con LiveData), y es muy evidente que Flow es la solución natural.

La solución nativa del lenguaje, integrado con corrutinas, e independiente del framework de Android, han venido para quedarse, y con esta serie de artículos, espero que te quede más claro cómo utilizarlos.

Si te queda cualquier duda o quieres que amplíe algún tema, no dudes en usar los comentarios.

Quizá también te interese…

Cómo crear un backend en Kotlin usando Ktor

Cómo crear un backend en Kotlin usando Ktor

Ktor es un framework de servidor web ligero y rápido para Kotlin, desarrollado por JetBrains. Es ideal para crear aplicaciones web y servicios RESTful, y es muy fácil de usar y configurar. En este artículo, vamos a ver cómo crear un backend para una aplicación de...

Cómo simular una base de datos reactiva en Room con Fakes

Cómo simular una base de datos reactiva en Room con Fakes

En el desarrollo de aplicaciones móviles es muy común utilizar bases de datos para almacenar y gestionar la información que se utiliza en la aplicación. En el caso de Android, una de las opciones más populares es Room, una librería de persistencia de datos que...

Flows de Kotlin para implementar búsquedas en tiempo real

Flows de Kotlin para implementar búsquedas en tiempo real

En Android, los Flows de Kotlin son una manera de representar secuencias de datos asincrónicas que emiten valores de forma continua. Estos Flows pueden ser útiles en situaciones en las que deseamos escuchar eventos y procesar los resultados de forma asíncrona, como en...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de DevExpert, S.L.U., 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 *