Room con Flow, y un ejemplo de paginación
Antonio Leiva

Esto avanza, y si en el artículo anterior veíamos cómo usar Flow en un proyecto Android, aquí vamos a ir más allá e integrarlo con Flow.

En realidad la integración es extremadamente sencilla, pero vamos a construir un ejemplo en el que tenga un sentido real, como es el de la paginación.

Integración de Flow con Room

Desde las versiones más recientes de Room, podemos hacer que las peticiones devuelvan un Flow, de forma equivalente a como lo hacían con su homónimo LiveData.

La ventaja de usar Flow en vez de LiveData es que no acoplamos nuestras capas de lógica de negocio al framework de Android, lo que es una gran noticia.

El único cambio que necesitas es pasar de esto:

 @Query("SELECT * FROM Movie")
 suspend fun getAll(): List<Movie>

A esto:

@Query("SELECT * FROM Movie")
fun getAll(): Flow<List<Movie>>

Ya está. A partir de este momento, cada vez que se actualice la tabla involucrada en esta petición, se nos devolverán los nuevos valores.

¿Y esto es todo? Pues la verdad es que sí, pero vamos a hacer un ejemplo más complejo para probarlo

Vamos a hacer un sistema de paginación muy sencillo. Quizá en otro artículo pruebe la librería de Paging 3, que ahora también viene con soporte para Flow, pero de momento vamos a hacerlo a mano para entenderlo todo.

Adaptando el repository

Vas a tener que cambiar algunas cosas, porque ahora el LocalDataSource devuelve un Flow en vez de un listado:

interface LocalDataSource {
    ...
    fun getMovies(): Flow<List<Movie>>
}

El cambio en RoomDataSource es un poco enrevesado, porque tenemos que hacer la conversión de las películas al vuelo. Pero esto nos ayuda a ver que podemos transformar Flows en otros de forma sencilla:

override fun getMovies(): Flow<List<Movie>> =
    movieDao.getAll().map { movies ->
        movies.map { it.toDomainMovie() }
    }

Mapeamos el objeto a uno nuevo que va a ser el mapping de cada item al tipo del dominio.

A nivel de repositorio, es simplemente devolver el Flow que recuperamos desde base de datos:

fun getMovies(): Flow<List<Movie>> = localDataSource.getMovies()

Creando una paginación propia con Flow

Bien, ya tenemos el camino “de vuelta” desde la fuente de datos local (la base de datos), ¿pero cómo informamos al repositorio de que hay que cargar datos?

De arriba a abajo vamos viendo. Primero sería detectar que realmente necesitamos nuevos datos. Pero eso no se lo vamos a hacer decidir a la interfaz, sino que ésta solo va a informa al ViewModel del último elemento visible:

val layoutManager = recycler.layoutManager as GridLayoutManager

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

Y después, en el ViewModel, informamos al repository. Como la función es suspend, necesitamos llamarlo desde una corrutina:

fun notifyLastVisible(lastVisible: Int) {
    viewModelScope.launch {
        repository.checkRequireNewPage(lastVisible)
        _spinner.value = false
    }
}

Vamos a necesitar que la primera vez, cuando no hay datos, se notifique que estamos en la posición 0. Esto se hace en el init del ViewModel:

init {
    _spinner.value = true
    notifyLastVisible(0)
}

El repositorio va a comprobar cuántos elementos hay hasta ahora, y si necesita más, llamará a la fuente de datos remota. Esto se podría optimizar más cacheando algunos valores, pero vamos a mantenerlo simple:

suspend fun checkRequireNewPage(lastVisible: Int) {
    val size = localDataSource.size()
    if (lastVisible >= size - PAGE_THRESHOLD) {
        val page = size / PAGE_SIZE + 1
        val newMovies = remoteDataSource.getMovies(page)
        localDataSource.saveMovies(newMovies)
    }
}

Si el último elemento visible es mayor que el total menos un cierto umbral, cargamos nuevos items. Es mejor no esperar a que el valor sea el último, primero para hacer la experiencia más fluida, y segundo porque podemos perder valores por el camino y que no nos devuelva exactamente el último.

Al guardarse los nuevos valores en la base de datos, el Flow automáticamente notificará de los cambios a la UI, así que no tenemos que hacer nada.

Sí que necesitarás algunos cambios extra en la fuente remota, que te dejo aquí a modo de ejercicio:

interface RemoteDataSource {
    suspend fun getMovies(page: Int): List<Movie>
}
class TheMovieDbDataSource(private val apiKey: String) : RemoteDataSource {

    override suspend fun getMovies(page: Int): List<Movie> =
        TheMovieDb.service
            .listPopularMoviesAsync(apiKey, page)
            .results
            .map { it.toDomainMovie() }
}
interface TheMovieDbService {
    @GET("discover/movie?sort_by=popularity.desc")
    suspend fun listPopularMoviesAsync(
        @Query("api_key") apiKey: String,
        @Query("page") page: Int
    ): MovieDbResult
}

Houston, tenemos un problema

Si ahora ejecutas este código rápidamente, te darás cuenta de que hay páginas que se cargan 2 veces.

Esto es porque las notificaciones al repositorio van más rápidas que las peticiones al servidor, y por tanto, la corrutina que tenemos en el ViewModel se crea muchas veces, y todas ellas lanzan una nueva petición.

¿Qué podemos hacer para solucionar esto? Para ello, necesitamos aprender un nuevo concepto, que te voy a dejar para un siguiente capítulo.

Nos vamos a cargar un componente de los Architecture Components al que probablemente tenías mucho cariño 😄

¡Nos leemos en el siguiente artículo!

Quizá también te interese…

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

Cómo hacer tests de Corrutinas y Flows – Paso a Paso

Cómo hacer tests de Corrutinas y Flows – Paso a Paso

¡Vaya viaje por el que hemos pasado en estos artículos! Hace ya varios de ellos empezamos hablando sobre la programación reactiva con Flow, y hemos aprendido un montón de conceptos e ideas sobre cómo aplicarlos en el día a día. Pero nada de esto está completo si no...

Convertir cualquier callback en un Flow con CallbackFlow

Convertir cualquier callback en un Flow con CallbackFlow

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