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…

Los 7 mejores cursos online para aprender Android desde cero en 2021

Los 7 mejores cursos online para aprender Android desde cero en 2021

No hay que ser un genio para darse cuenta de que el sector del desarrollo de aplicaciones móviles está en auge y cada vez más gente busca aprender Android para iniciarse en esta profesión. Atraídos, cómo no, por la posibilidad de obtener un empleo estable, (muy) bien...

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. Los campos obligatorios están marcados con *

Acepto la política de privacidad *