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…

3 formas de pasar varios Listeners a un RecyclerView

3 formas de pasar varios Listeners a un RecyclerView

Seguro que has visto muchos ejemplos donde un RecyclerView recibe un listener para, por ejemplo realizar, una acción cuando se hace click en el elemento. class MoviesAdapter(private val listener: (Movie) -> Unit) : ListAdapter<Movie, MoviesAdapter.ViewHolder>(...)...

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

Clases y constructores en Kotlin con Android Studio

Clases y constructores en Kotlin con Android Studio

Veremos un repaso las clases y constructores de Kotlin para solventar esas inquietudes que nos surgen a la hora de seguir este curso, que pueden ser como funciona realmente y porque se presentan las clases de ese modo, como es la interacción y el constructor a la hora...

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 *