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
PagingSourcedo Room - Quando o Room se aproxima do fim, o
RemoteMediatorbusca 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 directlyPagingSource 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áginasSuccess(endOfPaginationReached = false)quando ainda puderem existir mais páginasError(e)em caso de falha — exibido como umLoadState.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() }
).flowConsultando 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
PagingSourcedo Room — a fonte única da verdade RemoteMediator.load()lida comREFRESH/PREPEND/APPENDe grava os resultados no Room dentro de uma transação- Uma tabela de chaves remotas persiste as chaves seguinte/anterior entre reinicializações
- O
Pagerrecebe tanto umremoteMediatorquanto umpagingSourceFactory 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
- Por que usar paginação
- PagingSource e Pager
- Paginação em listas do Compose
- RemoteMediator e cache