0Pricing
Android Academy · Aula

RemoteMediator e cache

Combine paginação de rede e de banco de dados.

RemoteMediator e cache é uma aula grátis de Android Academy no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Android Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Android Academy inclui 4 aulas no total.

Paginação com prioridade ao modo offline

Um PagingSource de rede simples funciona, mas, se a conexão cair, a lista ficará vazia. Para ter um aplicativo robusto, é melhor usar o banco de dados como fonte única da verdade e deixar que a rede o preencha em segundo plano.

É exatamente isso que o RemoteMediator permite: paginar a partir do banco de dados local e buscar mais dados na rede quando o banco ficar sem conteúdo.

A arquitetura

Com o RemoteMediator, os dados fluem em uma única direção:

  • A interface do usuário pagina a partir de um PagingSource do Room
  • Quando o Room se aproxima do fim, o RemoteMediator busca a próxima página na rede
  • O resultado da rede é gravado no Room
  • O Room emite as novas linhas, e a interface é atualizada

O usuário sempre vê os dados armazenados em cache instantaneamente, mesmo sem conexão.

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

PagingSource do Room sem esforço

O Room pode gerar um PagingSource automaticamente. Basta declarar uma consulta que retorne PagingSource<Int, Entity>, e o Room a implementará.

Essa é a fonte a partir da qual a interface realmente pagina.

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

Criando uma subclasse de RemoteMediator

RemoteMediator<Key, Value> tem um método obrigatório, load(), além de um initialize() opcional.

load() recebe um LoadType (REFRESH, PREPEND ou APPEND) e o PagingState atual, e então retorna um 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
}

Lidando com LoadType

Em load(), você decide qual página buscar com base no LoadType:

  • REFRESH — carrega a primeira página (ou a página próxima à âncora)
  • PREPEND — geralmente não há nada a fazer; retorne endOfPaginationReached = true
  • APPEND — carrega a página depois da última que temos
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
    }
}

Tabela de chaves remotas

Ao contrário de um PagingSource, um RemoteMediator não pode retornar diretamente as chaves seguinte/anterior — elas precisam ser persistidas. Armazene-as em uma pequena tabela de chaves remotas, junto com os seus dados.

Isso permite retomar APPEND/PREPEND depois que o aplicativo for reiniciado.

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?
)

Gravando os resultados da rede no Room

Depois de buscar uma página, grave os itens e as respectivas chaves no Room dentro de uma transação. Em REFRESH, limpe primeiro as tabelas antigas para manter o cache consistente.

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() })
}

Retornando MediatorResult

Finalize load() retornando um MediatorResult:

  • Success(endOfPaginationReached = true) quando não houver mais páginas
  • Success(endOfPaginationReached = false) quando ainda puderem existir mais páginas
  • Error(e) em caso de falha — exibido como um 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)
}

Conectando o Pager ao remoteMediator

Crie o Pager com o remoteMediator e o pagingSourceFactory do Room. O mediador preenche o Room; a interface pagina a partir do Room.

Como essa funcionalidade usa uma API experimental, autorize seu uso com @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

Consultando chaves para APPEND

Para uma carga APPEND, você precisa do nextKey do último item carregado. Use a âncora do PagingState para encontrar a chave remota correta na tabela de chaves.

Funções auxiliares mantêm load() legível em REFRESH, APPEND e 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)

Armazenamento em cache e atualização

Como os dados ficam no Room, o aplicativo carrega instantaneamente e funciona sem conexão. Para evitar conteúdo desatualizado, substitua initialize() para decidir se os dados devem ser atualizados ao iniciar, com base na idade do cache.

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

Verificação rápida

Em uma configuração com RemoteMediator, qual é a fonte única da verdade a partir da qual a interface pagina?

Recapitulação: RemoteMediator e armazenamento em cache

Você criou um fluxo de paginação com prioridade ao modo offline:

  • A interface pagina a partir de um PagingSource do Room — a fonte única da verdade
  • RemoteMediator.load() lida com REFRESH/PREPEND/APPEND e grava os resultados no Room dentro de uma transação
  • Uma tabela de chaves remotas persiste as chaves seguinte/anterior entre reinicializações
  • O Pager recebe tanto um remoteMediator quanto um pagingSourceFactory
  • initialize() controla a atualização do cache

Agora você pode carregar listas infinitas com eficiência, com ou sem conexão. Parabéns por concluir o curso de Paging 3.

Perguntas Frequentes

A aula “RemoteMediator e cache” é grátis?

Sim — o texto completo de “RemoteMediator e cache” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Android Academy, atualize para CoddyKit PRO. O curso de Android Academy inclui 4 aulas no total.

O que vou aprender em “RemoteMediator e cache”?

Combine paginação de rede e de banco de dados. Você pratica Android Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar Android Academy?

Nenhuma experiência prévia é necessária. Android Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “RemoteMediator e cache”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de Android Academy?

Sim. Cada aula de Android Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Por que usar paginação
  2. PagingSource e Pager
  3. Paginação em listas do Compose
  4. RemoteMediator e cache
← Voltar para Android Academy