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