---
title: "Paging 3 com Kotlin e Compose: Listas Grandes em 2026 | Kotlin Brasil"
url: "https://kotlin.dev.br/blog/paging-3-kotlin-compose-2026/"
markdown_url: "https://kotlin.dev.br/blog/paging-3-kotlin-compose-2026.MD"
description: "Aprenda Paging 3 com Kotlin e Jetpack Compose: PagingSource, RemoteMediator, Room, Flow, load states, testes e boas práticas para listas grandes."
date: "2026-05-26"
author: "Karina Melo"
---

# Paging 3 com Kotlin e Compose: Listas Grandes em 2026 | Kotlin Brasil

Aprenda Paging 3 com Kotlin e Jetpack Compose: PagingSource, RemoteMediator, Room, Flow, load states, testes e boas práticas para listas grandes.


Listas grandes parecem simples até o app começar a travar, repetir itens, perder posição de rolagem ou baixar milhares de registros de uma vez. Em apps Android reais, feeds, catálogos, históricos, chats, pedidos, notificações, resultados de busca e telas administrativas raramente cabem em uma única resposta HTTP. É nesse ponto que **Paging 3 com Kotlin e Jetpack Compose** deixa de ser detalhe de performance e vira parte importante da arquitetura.

O Paging 3 é a biblioteca oficial do Android Jetpack para carregar dados em páginas, combinar fontes locais e remotas, expor fluxo reativo com `Flow<PagingData<T>>` e integrar com listas Compose por meio de `LazyPagingItems`. Quando usado bem, ele reduz consumo de memória, melhora tempo de primeira renderização e cria uma experiência mais estável em conexões brasileiras comuns: 4G instável, Wi-Fi público, rede corporativa com proxy e dispositivos intermediários.

Este guia mostra quando usar Paging 3, como criar um `PagingSource`, como integrar com Jetpack Compose, quando usar `RemoteMediator` com Room, como tratar `LoadState`, quais erros evitar e como testar a camada de paginação. Se você já estudou [Flow em Kotlin](/blog/kotlin-flow/), [Room Database](/tutoriais/kotlin-room-database-tutorial/), [Android offline-first](/blog/android-offline-first-kotlin-2026/) e [WorkManager](/blog/workmanager-kotlin-android-2026/), o Paging 3 completa uma peça essencial para apps Android modernos.

## Quando usar Paging 3?

Use Paging 3 quando a tela trabalha com uma coleção potencialmente grande ou dinâmica. Alguns exemplos comuns:

- feed de posts, produtos, notícias ou transações;
- busca com muitos resultados;
- histórico de pedidos, corridas, pagamentos ou mensagens;
- catálogo offline sincronizado em partes;
- lista de usuários, clientes, tarefas ou chamados;
- timeline ordenada por data, popularidade ou relevância.

Não use Paging 3 para listas pequenas e estáticas. Se a tela sempre exibe 10 categorias fixas, uma lista comum é mais simples. Também não vale complicar um formulário com paginação apenas porque a API suporta `page` e `limit`. A pergunta prática é: **a lista pode crescer a ponto de afetar memória, rede, tempo de resposta ou experiência de rolagem?** Se sim, Paging 3 merece entrar na arquitetura.

## Dependências básicas

Em um app Android com Gradle Kotlin DSL, você normalmente adiciona as integrações de runtime e Compose:

```kotlin
dependencies {
    implementation("androidx.paging:paging-runtime-ktx:3.3.6")
    implementation("androidx.paging:paging-compose:3.3.6")
}
```

Confira a versão mais recente compatível com o seu projeto, especialmente se você usa Kotlin, Compose Compiler e Android Gradle Plugin em versões novas. Em projetos com Room, a integração costuma ficar ainda mais simples porque DAOs podem retornar `PagingSource` diretamente.

## O modelo mental do Paging 3

O Paging 3 separa responsabilidades em três pontos principais:

- `PagingSource`: sabe carregar uma página a partir de uma chave;
- `Pager`: configura tamanho da página, placeholders e fonte de dados;
- `PagingData`: fluxo de dados paginados consumido pela UI.

Em um caso simples, o repository expõe um `Flow<PagingData<Item>>`. O ViewModel aplica cache no `viewModelScope`. A tela Compose coleta esse fluxo com `collectAsLazyPagingItems()` e renderiza usando `LazyColumn`.

Essa separação combina bem com [MVVM em Kotlin](/tutoriais/kotlin-mvvm-tutorial/) porque a UI não precisa conhecer detalhes de página, offset, cursor, retry HTTP ou banco local. Ela observa um estado paginado e reage a carregamento, erro e conteúdo vazio.

## PagingSource com API remota

Um `PagingSource` recebe uma chave e devolve uma página. A chave pode ser número da página, offset, timestamp, cursor ou token enviado pela API. Para APIs tradicionais com `page`, o código fica assim:

```kotlin
class ProdutosPagingSource(
    private val api: ProdutosApi,
    private val termo: String,
) : PagingSource<Int, Produto>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Produto> {
        val pagina = params.key ?: 1

        return try {
            val resposta = api.buscarProdutos(
                query = termo,
                page = pagina,
                pageSize = params.loadSize,
            )

            LoadResult.Page(
                data = resposta.items,
                prevKey = if (pagina == 1) null else pagina - 1,
                nextKey = if (resposta.items.isEmpty()) null else pagina + 1,
            )
        } catch (erro: IOException) {
            LoadResult.Error(erro)
        } catch (erro: HttpException) {
            LoadResult.Error(erro)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Produto>): Int? {
        return state.anchorPosition?.let { anchor ->
            val paginaMaisProxima = state.closestPageToPosition(anchor)
            paginaMaisProxima?.prevKey?.plus(1) ?: paginaMaisProxima?.nextKey?.minus(1)
        }
    }
}
```

O método `getRefreshKey` parece burocrático, mas é importante para manter a posição aproximada do usuário quando a lista é atualizada. Sem ele, refreshes podem voltar para o começo da lista de forma irritante.

## Repository e ViewModel

O repository monta o `Pager` e esconde a implementação da UI:

```kotlin
class ProdutosRepository(
    private val api: ProdutosApi,
) {
    fun buscarProdutos(termo: String): Flow<PagingData<Produto>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                initialLoadSize = 40,
                prefetchDistance = 5,
                enablePlaceholders = false,
            ),
            pagingSourceFactory = {
                ProdutosPagingSource(api = api, termo = termo)
            },
        ).flow
    }
}
```

No ViewModel, use `cachedIn(viewModelScope)` para evitar recarregamento desnecessário quando a tela recompõe ou quando há múltiplos collectors:

```kotlin
class ProdutosViewModel(
    private val repository: ProdutosRepository,
) : ViewModel() {

    private val termoBusca = MutableStateFlow("")

    val produtos: Flow<PagingData<Produto>> = termoBusca
        .debounce(300)
        .distinctUntilChanged()
        .flatMapLatest { termo -> repository.buscarProdutos(termo) }
        .cachedIn(viewModelScope)

    fun atualizarBusca(novoTermo: String) {
        termoBusca.value = novoTermo
    }
}
```

Esse padrão é útil para busca porque cada termo cria uma nova fonte de paginação. `flatMapLatest` cancela a busca anterior quando o usuário digita algo novo, evitando corrida entre respostas antigas e novas.

## Consumindo PagingData no Jetpack Compose

No Compose, a integração fica direta:

```kotlin
@Composable
fun ProdutosScreen(
    viewModel: ProdutosViewModel,
) {
    val produtos = viewModel.produtos.collectAsLazyPagingItems()

    LazyColumn {
        items(
            count = produtos.itemCount,
            key = produtos.itemKey { it.id },
        ) { index ->
            val produto = produtos[index]
            if (produto != null) {
                ProdutoCard(produto = produto)
            } else {
                ProdutoPlaceholder()
            }
        }
    }
}
```

Use `key` estável sempre que possível. Isso ajuda o Compose a preservar estado de itens, animações e posição de rolagem. Se você usa `id` vindo do backend, prefira esse identificador em vez do índice da lista.

## Tratando loading, erro e lista vazia

Uma tela paginada precisa diferenciar três estados:

- carregamento inicial (`refresh`);
- carregamento no final da lista (`append`);
- erro inicial ou erro ao buscar a próxima página.

```kotlin
@Composable
fun ProdutosContent(produtos: LazyPagingItems<Produto>) {
    when (val refresh = produtos.loadState.refresh) {
        is LoadState.Loading -> LoadingTelaInteira()
        is LoadState.Error -> ErroTelaInteira(
            mensagem = refresh.error.message ?: "Não foi possível carregar os produtos.",
            onRetry = { produtos.retry() },
        )
        is LoadState.NotLoading -> {
            if (produtos.itemCount == 0) {
                EmptyState(mensagem = "Nenhum produto encontrado.")
            } else {
                ListaProdutos(produtos)
            }
        }
    }
}
```

Para `append`, o ideal é renderizar um item no rodapé:

```kotlin
if (produtos.loadState.append is LoadState.Loading) {
    item { LoadingRodape() }
}

if (produtos.loadState.append is LoadState.Error) {
    item { BotaoTentarNovamente(onClick = { produtos.retry() }) }
}
```

Essa diferença melhora bastante a experiência. Um erro na próxima página não deve apagar os itens já carregados. A lista pode continuar visível, com um retry discreto no rodapé.

## Paging 3 com Room e RemoteMediator

Para apps offline-first, o padrão mais robusto é usar Room como fonte local e `RemoteMediator` para sincronizar rede e banco. A UI lê do banco. O mediator decide quando buscar mais dados e salvar localmente. Isso combina com a arquitetura descrita no guia de [Android offline-first com Kotlin](/blog/android-offline-first-kotlin-2026/).

O DAO pode retornar `PagingSource`:

```kotlin
@Dao
interface ProdutoDao {
    @Query("SELECT * FROM produtos ORDER BY nome ASC")
    fun paginarProdutos(): PagingSource<Int, ProdutoEntity>
}
```

O repository usa o banco como fonte principal:

```kotlin
fun produtosOfflineFirst(): Flow<PagingData<Produto>> {
    return Pager(
        config = PagingConfig(pageSize = 30),
        remoteMediator = ProdutosRemoteMediator(api, database),
        pagingSourceFactory = { database.produtoDao().paginarProdutos() },
    ).flow.map { pagingData ->
        pagingData.map { entity -> entity.toDomain() }
    }
}
```

Esse desenho evita que a tela dependa diretamente da rede. Se o usuário abre o app sem conexão, os dados locais continuam aparecendo. Quando a rede volta, o mediator atualiza o banco e a lista reflete as mudanças automaticamente.

## Tamanho de página, prefetch e placeholders

Não existe um tamanho perfeito de página. Comece com algo entre 20 e 50 itens e ajuste medindo tempo de resposta, tamanho do payload e custo de renderização. Páginas muito pequenas aumentam overhead de rede. Páginas grandes demais atrasam o primeiro carregamento e podem causar jank em dispositivos modestos.

`prefetchDistance` define quando o Paging começa a buscar a próxima página antes de o usuário chegar ao fim. Um valor baixo economiza rede, mas pode mostrar loading com frequência. Um valor alto deixa a rolagem mais fluida, mas pode baixar dados que o usuário nunca verá.

`enablePlaceholders` só faz sentido quando você sabe o tamanho total da lista e quer reservar espaço para itens ainda não carregados. Em muitos feeds e buscas, `false` é mais previsível.

## Erros comuns em projetos Kotlin

O primeiro erro é recriar o `Pager` a cada recomposição. O `Pager` deve ficar no repository ou no ViewModel, não dentro de um `@Composable` comum. Compose pode recompor muitas vezes; paginação não deve reiniciar por causa disso.

O segundo erro é esquecer `cachedIn(viewModelScope)`. Sem cache, mudanças de configuração e novos collectors podem disparar novas cargas desnecessárias.

O terceiro erro é tratar todo erro como tela cheia. Se o erro acontece no `append`, preserve a lista e ofereça retry no rodapé.

O quarto erro é misturar filtros sem invalidar corretamente a fonte. Se busca, ordenação ou categoria mudam, crie um novo fluxo de paginação com `flatMapLatest` ou invalide o `PagingSource` de forma explícita.

O quinto erro é ignorar testes. Paginação parece visual, mas boa parte da lógica está em `PagingSource`, repository e mediator. Isso pode ser testado na JVM.

## Testando PagingSource

Um teste simples valida se a primeira página retorna os dados esperados:

```kotlin
@Test
fun `deve carregar primeira pagina de produtos`() = runTest {
    val api = FakeProdutosApi(
        produtos = listOf(
            Produto(id = "1", nome = "Café"),
            Produto(id = "2", nome = "Chá"),
        ),
    )

    val source = ProdutosPagingSource(api = api, termo = "")

    val resultado = source.load(
        PagingSource.LoadParams.Refresh(
            key = null,
            loadSize = 20,
            placeholdersEnabled = false,
        ),
    )

    assertEquals(
        PagingSource.LoadResult.Page(
            data = api.produtos,
            prevKey = null,
            nextKey = 2,
        ),
        resultado,
    )
}
```

Em mediators, teste cenários de refresh, append, erro HTTP, banco vazio e banco já populado. Para fluxos com `PagingData`, use ferramentas do próprio Paging e, quando fizer sentido, combine com estratégias de teste já abordadas no [guia de testes Android com Kotlin, Compose e Maestro](/guias/testes-android-compose-maestro/).

## Como isso ajuda carreira e produto

Paging 3 aparece com frequência em apps usados em produção porque resolve um problema que quase toda empresa tem: listas grandes com rede imperfeita. Para carreira Android, saber explicar `PagingSource`, `RemoteMediator`, `LoadState`, Room e Compose em conjunto demonstra maturidade além do CRUD básico.

Para produto, a vantagem é concreta: menos travamento, menos dados baixados sem necessidade, primeira tela mais rápida e experiência melhor quando o usuário navega por catálogos, históricos ou feeds extensos. Em times backend, esse mesmo cuidado força contratos de API melhores, com paginação consistente por cursor, metadados claros e respostas previsíveis. Se você trabalha também no servidor, vale comparar esse desenho com APIs Kotlin em [Ktor](/blog/ktor-criando-apis-kotlin/) ou com alternativas de alta performance em <a href="https://golang.com.br/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go para backend</a>.

## Conclusão

Paging 3 não é apenas uma biblioteca para “carregar mais itens”. Ele organiza uma parte crítica da experiência Android: como dados grandes entram na tela sem desperdiçar rede, memória ou paciência do usuário. Em Kotlin, a combinação de `Flow`, `PagingData`, Room, `RemoteMediator` e Jetpack Compose cria uma arquitetura moderna e testável para listas grandes.

Comece simples com `PagingSource` quando a API remota já resolve o caso. Evolua para Room e `RemoteMediator` quando a experiência precisa ser offline-first. Trate `LoadState` com cuidado, preserve itens já carregados em erros de próxima página e mantenha o `Pager` fora da recomposição. Esses detalhes separam uma lista que “funciona no demo” de uma tela pronta para produção em 2026.
