0Pricing
Android Academy · Lección

RemoteMediator y caché

Combine la paginación de red y de base de datos

RemoteMediator y caché es una lección gratuita de Android Academy en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Android Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Android Academy incluye 4 lecciones en total.

Paginación con prioridad offline

Un PagingSource de red básico funciona, pero si se pierde la conexión, la lista queda vacía. Para una aplicación robusta, conviene que la base de datos sea la única fuente de verdad y que la red la complete en segundo plano.

Eso es exactamente lo que permite RemoteMediator: paginar desde la base de datos local y obtener más datos de la red cuando se agotan los de la base de datos.

La arquitectura

Con RemoteMediator, los datos fluyen en una sola dirección:

  • La interfaz de usuario pagina desde un PagingSource de Room
  • Cuando Room se acerca al final, RemoteMediator obtiene la siguiente página de la red
  • El resultado de la red se escribe en Room
  • Room emite las nuevas filas y la interfaz se actualiza

El usuario siempre ve los datos almacenados en caché al instante, incluso sin conexión.

// Network ---> RemoteMediator ---> Room (source of truth) ---> UI
// UI never reads the network directly

PagingSource de Room sin esfuerzo

Room puede generar un PagingSource automáticamente. Solo tiene que declarar una consulta que devuelva PagingSource<Int, Entity> y Room se encargará de implementarlo.

Esta es la fuente desde la que realmente pagina la interfaz de usuario.

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY position ASC")
    fun pagingSource(): PagingSource<Int, ArticleEntity>
}

Crear una subclase de RemoteMediator

RemoteMediator<Key, Value> tiene un método obligatorio, load(), y un método opcional, initialize().

load() recibe un LoadType (REFRESH, PREPEND o APPEND) y el PagingState actual, y devuelve un MediatorResult.

import androidx.paging.ExperimentalPagingApi
import androidx.paging.RemoteMediator

@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
    private val api: ArticleApi,
    private val db: AppDatabase
) : RemoteMediator<Int, ArticleEntity>() {
    // override suspend fun load(loadType, state): MediatorResult
}

Gestionar LoadType

En load(), decide qué página obtener según el LoadType:

  • REFRESH: carga la primera página (o la que está alrededor del ancla)
  • PREPEND: normalmente no hay nada que hacer; devuelva endOfPaginationReached = true
  • APPEND: carga la página posterior a la última que tenemos
val page: Int = when (loadType) {
    LoadType.REFRESH -> 1
    LoadType.PREPEND ->
        return MediatorResult.Success(endOfPaginationReached = true)
    LoadType.APPEND -> {
        val lastKey = db.remoteKeyDao().last()?.nextKey
            ?: return MediatorResult.Success(endOfPaginationReached = true)
        lastKey
    }
}

Tabla de claves remotas

A diferencia de un PagingSource, un RemoteMediator no puede devolver directamente las claves siguiente/anterior; es necesario conservarlas. Guárdelas en una pequeña tabla de claves remotas junto con los datos.

Esto permite reanudar APPEND/PREPEND después de reiniciar la aplicación.

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "remote_keys")
data class RemoteKey(
    @PrimaryKey val articleId: Int,
    val prevKey: Int?,
    val nextKey: Int?
)

Escribir los resultados de red en Room

Después de obtener una página, escriba los elementos y sus claves en Room dentro de una transacción. En REFRESH, borre primero las tablas antiguas para que la caché siga siendo coherente.

db.withTransaction {
    if (loadType == LoadType.REFRESH) {
        db.remoteKeyDao().clearAll()
        db.articleDao().clearAll()
    }
    val nextKey = if (response.items.isEmpty()) null else page + 1
    db.remoteKeyDao().insertAll(
        response.items.map { RemoteKey(it.id, prevKey = page - 1, nextKey = nextKey) }
    )
    db.articleDao().insertAll(response.items.map { it.toEntity() })
}

Devolver MediatorResult

Finalice load() devolviendo un MediatorResult:

  • Success(endOfPaginationReached = true) cuando no quedan más páginas
  • Success(endOfPaginationReached = false) cuando puede haber más páginas
  • Error(e) en caso de error; se muestra como un LoadState.Error
return try {
    val response = api.getArticles(page = page, size = state.config.pageSize)
    db.withTransaction { /* write items + keys */ }
    MediatorResult.Success(
        endOfPaginationReached = response.items.isEmpty()
    )
} catch (e: IOException) {
    MediatorResult.Error(e)
} catch (e: HttpException) {
    MediatorResult.Error(e)
}

Conectar Pager con remoteMediator

Construya el Pager con remoteMediator y pagingSourceFactory de Room. El mediador completa Room; la interfaz pagina desde Room.

Como utiliza una API experimental, habilítela con @OptIn(ExperimentalPagingApi::class).

@OptIn(ExperimentalPagingApi::class)
fun articles(): Flow<PagingData<ArticleEntity>> = Pager(
    config = PagingConfig(pageSize = 20),
    remoteMediator = ArticleRemoteMediator(api, db),
    pagingSourceFactory = { db.articleDao().pagingSource() }
).flow

Buscar claves para APPEND

Para una carga APPEND, necesita la nextKey del último elemento cargado. Use el ancla de PagingState para encontrar la clave remota correcta en la tabla de claves.

Las funciones auxiliares mantienen la legibilidad de load() en REFRESH, APPEND y PREPEND.

private suspend fun lastRemoteKey(
    state: PagingState<Int, ArticleEntity>
): RemoteKey? {
    return state.pages.lastOrNull { it.data.isNotEmpty() }
        ?.data?.lastOrNull()
        ?.let { db.remoteKeyDao().keyFor(it.id) }
}

// In load(): for APPEND
val nextKey = lastRemoteKey(state)?.nextKey
    ?: return MediatorResult.Success(endOfPaginationReached = true)

Caché y actualización

Como los datos se encuentran en Room, la aplicación carga al instante y funciona sin conexión. Para evitar contenido obsoleto, sobrescriba initialize() y decida si debe actualizarse al iniciar según la antigüedad de la caché.

override suspend fun initialize(): InitializeAction {
    val lastUpdate = db.remoteKeyDao().lastUpdatedMillis() ?: 0L
    val cacheTimeout = TimeUnit.HOURS.toMillis(1)
    return if (System.currentTimeMillis() - lastUpdate >= cacheTimeout) {
        InitializeAction.LAUNCH_INITIAL_REFRESH
    } else {
        InitializeAction.SKIP_INITIAL_REFRESH
    }
}

Comprobación rápida

En una configuración con RemoteMediator, ¿cuál es la única fuente de verdad desde la que pagina la interfaz de usuario?

Resumen: RemoteMediator y almacenamiento en caché

Ha creado un flujo de paginación con prioridad offline:

  • La interfaz pagina desde un PagingSource de Room, la única fuente de verdad
  • RemoteMediator.load() gestiona REFRESH/PREPEND/APPEND y escribe los resultados en Room dentro de una transacción
  • Una tabla de claves remotas conserva las claves siguiente/anterior después de reiniciar la aplicación
  • Pager recibe tanto un remoteMediator como un pagingSourceFactory
  • initialize() controla la actualización de la caché

Ahora puede cargar listas infinitas de forma eficiente, con conexión o sin ella. Enhorabuena por completar el curso de Paging 3.

Preguntas frecuentes

¿La lección «RemoteMediator y caché» es gratis?

Sí — el texto completo de «RemoteMediator y caché» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Android Academy, actualiza a CoddyKit PRO. El curso de Android Academy incluye 4 lecciones en total.

¿Qué aprenderé en «RemoteMediator y caché»?

Combine la paginación de red y de base de datos Practicas Android Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar Android Academy?

No se requiere experiencia previa. Android Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «RemoteMediator y caché»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de Android Academy?

Sí. Cada lección de Android Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Por qué usar Paging
  2. PagingSource y Pager
  3. Paging en listas de Compose
  4. RemoteMediator y caché
← Volver a Android Academy