Noções básicas da Android Paging

1. Introdução

O que você vai aprender

  • Quais são os principais componentes da biblioteca Paging.
  • Como adicionar a biblioteca Paging ao seu projeto.

O que você vai criar

Neste codelab, você vai começar com um app de exemplo que já mostra uma lista de artigos. A lista é estática, tem 500 artigos e todos eles são mantidos na memória do smartphone:

7d256d9c74e3b3f5.png

Durante o codelab, vai aprender:

  • Sobre o conceito de paginação.
  • Sobre os principais componentes da biblioteca Paging.
  • A usar a biblioteca Paging para implementar a paginação.

Quando terminar, você vai ter um app que:

  • Implementa a paginação com sucesso.
  • Se comunica com eficiência com o usuário quando mais dados são buscados.

Veja uma visualização rápida da IU que usaremos:

6277154193f7580.gif

O que é necessário

Opcional

2. Configurar o ambiente

Nesta etapa, você vai fazer o download do código para o codelab inteiro e executar um app simples de exemplo.

Para começar o mais rápido possível, preparamos um projeto inicial para você desenvolver.

Se você tiver o git instalado, basta executar o comando abaixo. Para verificar se o git está instalado, digite git --version no terminal ou na linha de comando e confira se ele é executado corretamente.

 git clone https://github.com/googlecodelabs/android-paging

Caso não tenha o git, clique no botão abaixo para fazer o download de todo o código deste codelab:

O código é organizado em duas pastas, basic e advanced. Neste codelab, vamos usar apenas a pasta basic.

Na pasta basic, há também duas outras pastas: start e end. Começaremos a trabalhar no código na pasta start e, ao final do codelab, o código na pasta start vai ser idêntico ao usado na pasta end.

  1. Abra o projeto no diretório basic/start no Android Studio.
  2. Execute a configuração de execução app em um dispositivo ou emulador.

89af884fa2d4e709.png

Vamos ver uma lista de artigos. Role até o fim e veja se a lista é estática. Em outras palavras, mais itens não são buscados quando chegarmos ao fim da lista. Volte ao topo para conferir se todos os itens ainda estão lá.

3. Introdução à paginação

Uma das maneiras mais comuns de exibir informações aos usuários é usando listas. No entanto, às vezes, essas listas oferecem apenas uma pequena janela para todo o conteúdo disponível ao usuário. À medida que o usuário percorre as informações disponíveis, normalmente há a expectativa de que mais dados sejam buscados para complementar as informações que já foram vistas. Sempre que os dados são buscados, o processo precisa ser eficiente e simples para que os carregamentos incrementais não prejudiquem a experiência do usuário. Os carregamentos incrementais também oferecem um benefício de desempenho porque o app não precisa armazenar grandes quantidades de dados na memória de uma só vez.

Esse processo de busca de informações é chamado de paginação, em que cada página corresponde a um bloco de dados que é buscado. Para solicitar uma página, a fonte de dados que está sendo paginada precisa ter uma consulta que defina as informações necessárias. O restante deste codelab vai apresentar a biblioteca Paging e demonstrar como ela ajuda a implementar a paginação de forma rápida e eficiente no app.

Principais componentes da biblioteca Paging

Os principais componentes da biblioteca Paging são:

  • PagingSource: a classe base para carregar blocos de dados de uma consulta de página específica. Ela faz parte da camada de dados e normalmente é exposta por uma classe DataSource e, depois, pelo Repository para uso no ViewModel.
  • PagingConfig: uma classe que define os parâmetros que determinam o comportamento da paginação. Isso inclui o tamanho da página, se os marcadores de posição estão ativados e assim por diante.
  • Pager: uma classe responsável por produzir o fluxo de PagingData. Ela depende da PagingSource para fazer isso e precisa ser criada no ViewModel.
  • PagingData: contêiner para dados paginados. Cada atualização de dados tem uma emissão de PagingData separada correspondente, com suporte da própria PagingSource.
  • PagingDataAdapter: uma subclasse de RecyclerView.Adapter que mostra PagingData em uma RecyclerView. O PagingDataAdapter pode ser conectado a um Flow do Kotlin, LiveData, um Flowable do RxJava, um Observable do RxJava ou até mesmo a uma lista estática usando métodos de fábrica. O PagingDataAdapter detecta eventos de carregamento internos de PagingData e atualiza a IU de forma eficiente à medida que as páginas são carregadas.

566d0f6506f39480.jpeg

Nas próximas seções, você vai implementar exemplos de cada um dos componentes descritos acima.

4. Visão geral do projeto

O app no formato atual exibe uma lista estática de artigos. Cada artigo tem um título, uma descrição e uma data de criação. Uma lista estática funciona bem para um pequeno número de itens, mas não é bem escalonada quando os conjuntos de dados ficam maiores. Vamos corrigir esse problema implementando a paginação com a biblioteca Paging, mas primeiro vamos analisar os componentes que já estão no app.

O app segue a arquitetura recomendada no Guia para a arquitetura do app. Veja o que você vai encontrar em cada pacote:

Camada de dados:

  • ArticleRepository: responsável por fornecer a lista de artigos e armazená-la na memória.
  • Article: uma classe que representa o modelo de dados, uma representação das informações extraídas da camada de dados.

Camada de IU:

  • Activity, RecyclerView.Adapter e RecyclerView.ViewHolder: classes responsáveis por exibir a lista na IU.
  • ViewModel: o holder do estado responsável por criar o estado que a IU precisa exibir.

O repositório expõe todos os artigos em um Flow com o campo articleStream. O fluxo é lido pelo ArticleViewModel na camada da IU, que o prepara para consumo pela IU na ArticleActivity com o campo state, um StateFlow (link em inglês).

A exposição de artigos como um Flow no repositório permite que o repositório atualize os artigos apresentados conforme eles mudam ao longo do tempo. Por exemplo, se o título de um artigo mudar, essa mudança pode ser facilmente comunicada aos coletores de articleStream. O uso de um StateFlow para o estado da IU no ViewModel garante que, mesmo se pararmos de coletar o estado da IU (por exemplo, quando a Activity for recriada durante uma mudança de configuração), vamos continuar imediatamente de onde paramos ao começar a coletá-lo novamente.

Como mencionado anteriormente, o articleStream atual no repositório apresenta somente notícias do dia atual. Embora isso seja suficiente para alguns usuários, outros talvez queiram visualizar artigos mais antigos ao percorrer todos os artigos disponíveis para o dia atual. Essa expectativa faz com que a exibição de artigos seja um ótimo candidato para paginação. Veja outros motivos para usar a paginação nos artigos:

  • O ViewModel mantém todos os itens carregados na memória no items StateFlow. Essa é uma grande preocupação quando o conjunto de dados fica muito grande, porque pode afetar o desempenho.
  • Quanto maior a lista de artigos, mais caro do ponto de vista computacional fica atualizar um ou mais artigos na lista quando eles mudam.

A biblioteca Paging ajuda a resolver todos esses problemas, além de oferecer uma API consistente para buscar dados de maneira incremental (paginação) nos apps.

5. Definir a origem dos dados

Ao implementar a paginação, queremos garantir que as condições abaixo sejam atendidas:

  • Processamento correto de solicitações de dados da IU, garantindo que várias solicitações não sejam acionadas ao mesmo tempo na mesma consulta.
  • Manter uma quantidade gerenciável de dados armazenados na memória.
  • Acionamento de solicitações para buscar mais dados e complementar os dados que já buscamos.

Podemos fazer tudo isso com uma PagingSource. Uma PagingSource define a origem dos dados e especifica como extrair dados em blocos incrementais. O objeto PagingData extrai dados da PagingSource em resposta a dicas de carregamento que são geradas conforme o usuário rola a tela em uma RecyclerView.

Nossa PagingSource vai carregar artigos. Em data/Article.kt, você vai encontrar o modelo definido desta maneira ::

data class Article(
    val id: Int,
    val title: String,
    val description: String,
    val created: LocalDateTime,
)

Para criar a PagingSource, é necessário definir o seguinte:

  • O tipo de chave de paginação: a definição do tipo de consulta de página que vamos usar para solicitar mais dados. Em nosso caso, buscamos artigos depois ou antes de um determinado ID de artigo, porque os IDs são ordenados em ordem crescente.
  • O tipo de dados carregados: cada página retorna uma List de artigos. Portanto, o tipo é Article.
  • Origem dos dados: normalmente, um banco de dados, um recurso de rede ou qualquer outra origem de dados paginados. No entanto, neste codelab, estamos usando dados gerados localmente.

No pacote data, vamos criar uma implementação de PagingSource em um novo arquivo com o nome ArticlePagingSource.kt:

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource : PagingSource<Int, Article>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        TODO("Not yet implemented")
    }
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        TODO("Not yet implemented")
    }
}

A PagingSource requer a implementação de duas funções: load() e getRefreshKey().

A função load() vai ser chamada pela biblioteca Paging para buscar de forma assíncrona mais dados que serão exibidos à medida que o usuário rolar a tela. O objeto LoadParams mantém as informações relacionadas à operação de carregamento, incluindo o seguinte:

  • A chave da página a ser carregada: se for a primeira vez que a função load() é chamada, LoadParams.key vai ser null. Nesse caso, será necessário definir a chave da página inicial. Para o nosso projeto, usamos o ID do artigo como a chave. Também adicionaremos uma constante STARTING_KEY de 0 à parte de cima do arquivo ArticlePagingSource para a chave de página inicial.
  • Tamanho do carregamento: o número de itens solicitados a serem carregados.

A função load() retorna um LoadResult. O LoadResult pode ser um destes tipos:

  • LoadResult.Page, se o resultado for bem-sucedido.
  • LoadResult.Error, em caso de erro.
  • LoadResult.Invalid, se a PagingSource for invalidada porque não pode mais garantir a integridade dos resultados.

Uma LoadResult.Page tem três argumentos obrigatórios:

  • data: uma List dos itens buscados.
  • prevKey: a chave usada pelo método load() se ele precisar buscar itens antes da página atual.
  • nextKey: a chave usada pelo método load() se ele precisar buscar itens depois da página atual.

E dois argumentos opcionais:

  • itemsBefore: o número de marcadores de posição que são exibidos antes dos dados carregados.
  • itemsAfter: o número de marcadores de posição que são exibidos depois dos dados carregados.

Nossa chave de carregamento é o campo Article.id. Podemos usar o campo como chave porque o ID de Article aumenta em um para cada artigo. Isso significa que os IDs de artigo são números inteiros consecutivos que aumentam monotonicamente.

A nextKey ou a prevKey é null se não houver mais dados a serem carregados na direção correspondente. Em nosso caso, para prevKey:

  • Se a startKey for igual a STARTING_KEY, retornaremos o valor nulo, já que não é possível carregar mais itens antes dessa chave.
  • Caso contrário, vamos colocar o primeiro item na lista e carregar LoadParams.loadSize atrás dele, garantindo que nunca haja uma chave menor que STARTING_KEY. Para isso, definimos o método ensureValidKey().

Adicione a função abaixo que verifica se a chave de paginação é válida:

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
   /**
     * Makes sure the paging key is never less than [STARTING_KEY]
     */
    private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}

Para nextKey:

  • Como aceitamos o carregamento de itens infinitos, transmitimos range.last + 1.

Além disso, como cada artigo tem um campo created, também precisamos gerar um valor para eles. Adicione o código abaixo à parte de cima do arquivo:

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
}

Com todo esse código definido, agora podemos implementar a função load():

import kotlin.math.max
...

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        // Start paging with the STARTING_KEY if this is the first load
        val start = params.key ?: STARTING_KEY
        // Load as many items as hinted by params.loadSize
        val range = start.until(start + params.loadSize)

        return LoadResult.Page(
            data = range.map { number ->
                Article(
                    // Generate consecutive increasing numbers as the article id
                    id = number,
                    title = "Article $number",
                    description = "This describes article $number",
                    created = firstArticleCreatedTime.minusDays(number.toLong())
                )
            },

            // Make sure we don't try to load items behind the STARTING_KEY
            prevKey = when (start) {
                STARTING_KEY -> null
                else -> ensureValidKey(key = range.first - params.loadSize)
            },
            nextKey = range.last + 1
        )
    }

    ...
}

Em seguida, precisamos implementar getRefreshKey(). Esse método é chamado quando a biblioteca Paging precisa recarregar itens para a IU porque os dados na PagingSource de suporte mudaram. A situação em que os dados de uma PagingSource mudam e precisam ser atualizados na IU é chamada de invalidação. Quando invalidada, a biblioteca Paging cria uma nova PagingSource para recarregar os dados e informa a IU emitindo uma nova PagingData. Aprenderemos mais sobre invalidação em uma seção mais adiante.

Ao carregar de uma nova PagingSource, a função getRefreshKey() é chamada para fornecer a chave com que a nova PagingSource precisa começar a carregar para garantir que o usuário não perca o lugar atual na lista após a atualização.

A invalidação da biblioteca Paging ocorre por um destes dois motivos:

  • Você chamou refresh() no PagingAdapter.
  • Você chamou invalidate() na PagingSource.

A chave retornada (no nosso caso, um Int) vai ser transmitida para a próxima chamada do método load() na nova PagingSource usando o argumento LoadParams. Para evitar que os itens sejam pulados após a invalidação, precisamos garantir que a chave retornada carregue itens suficientes para preencher a tela. Isso aumenta a possibilidade do novo conjunto incluir itens presentes nos dados invalidados, o que ajuda a manter a posição de rolagem atual. Vamos analisar a implementação no app:

   // The refresh key is used for the initial load of the next PagingSource, after invalidation
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        // In our case we grab the item closest to the anchor position
        // then return its id - (state.config.pageSize / 2) as a buffer
        val anchorPosition = state.anchorPosition ?: return null
        val article = state.closestItemToPosition(anchorPosition) ?: return null
        return ensureValidKey(key = article.id - (state.config.pageSize / 2))
    }

No snippet acima, usamos PagingState.anchorPosition. Você já se perguntou como a biblioteca Paging sabe buscar mais itens? Isso é uma pista! Quando a IU tenta ler itens de PagingData, ela tenta ler um determinado índice. Se os dados forem lidos, eles são exibidos na IU. No entanto, se não houver dados, a biblioteca Paging sabe que precisa buscar dados para atender à solicitação de leitura com falha. O último índice que buscou dados durante a leitura é a anchorPosition.

Ao atualizar a exibição, usamos a chave do Article mais próxima da anchorPosition como chave de carregamento. Dessa forma, quando começarmos a carregar novamente de uma nova PagingSource, o conjunto de itens buscados vai incluir itens que já foram carregados, o que garante uma experiência do usuário suave e consistente.

Com isso, você definiu totalmente uma PagingSource. A próxima etapa é conectá-la à IU.

6. Produzir PagingData para a IU

Na implementação atual, usamos um Flow<List<Article>> no ArticleRepository para expor os dados carregados no ViewModel. O ViewModel mantém um estado sempre disponível dos dados com o operador stateIn para exposição à IU.

Com a biblioteca Paging, vamos expor um Flow<PagingData<Article>> no ViewModel. PagingData é um tipo que une os dados carregados e ajuda a biblioteca Paging a decidir quando buscar mais dados. Além disso, ele garante que a mesma página não seja solicitada duas vezes.

Para construir PagingData, vamos usar um dos vários métodos diferentes do builder da classe Pager, dependendo da API que queremos usar para transmitir os PagingData a outras camadas do app:

  • Flow do Kotlin: use Pager.flow.
  • LiveData: use Pager.liveData.
  • Flowable RxJava: use Pager.flowable.
  • Observable RxJava: use Pager.observable.

Como já estamos usando o Flow em nosso app, continuaremos com essa abordagem. Mas, em vez de usarmos o Flow<List<Article>>, usaremos o Flow<PagingData<Article>>.

Independentemente do builder PagingData usado, será necessário transmitir os seguintes parâmetros:

  • PagingConfig. Essa classe define opções sobre a forma de carregamento do conteúdo de uma PagingSource. Por exemplo, até onde o conteúdo vai ser carregado antecipadamente, a solicitação de tamanho do carregamento inicial, entre outras. O único parâmetro obrigatório a ser definido é o tamanho da página, ou seja, quantos itens são carregados em cada página. Por padrão, a biblioteca Paging mantém todas as páginas carregadas na memória. Para garantir que você não desperdice memória conforme o usuário rola a tela, defina o parâmetro maxSize em PagingConfig. Por padrão, a Paging retorna itens nulos como um marcador de conteúdo que ainda não foi carregado se ela puder contar os itens descarregados e se a sinalização de configuração enablePlaceholders for true. Dessa forma, você pode mostrar uma visualização de marcador de posição no adaptador. Para simplificar o trabalho neste codelab, vamos desativar os marcadores transmitindo enablePlaceholders = false.
  • Uma função que define como criar a PagingSource. Em nosso caso, vamos criar uma ArticlePagingSource, então precisamos de uma função que informe à biblioteca Paging como fazer isso.

Agora, vamos modificar a classe ArticleRepository.

Atualizar ArticleRepository

  • Exclua o campo articlesStream.
  • Adicione um método com o nome articlePagingSource() que retorna a ArticlePagingSource que acabamos de criar.
class ArticleRepository) {

    fun articlePagingSource() = ArticlePagingSource()
}

Limpar ArticleRepository

A biblioteca Paging faz muitas coisas:

  • Processa a memória em cache.
  • Solicita dados quando o usuário chega perto do fim da lista.

Isso significa que todo o restante no ArticleRepository pode ser removido, exceto articlePagingSource(). O arquivo ArticleRepository vai ficar assim:

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource

class ArticleRepository {
    fun articlePagingSource() = ArticlePagingSource()
}

Você vai notar erros de compilação no ArticleViewModel. Vamos ver quais mudanças precisam ser feitas.

7. Solicitar e armazenar em cache o PagingData no ViewModel

Antes de solucionar os erros de compilação, vamos analisar o ViewModel.

class ArticleViewModel(...) : ViewModel() {

    val items: StateFlow<List<Article>> = ...
}

Para integrar a biblioteca Paging ao ViewModel, vamos mudar o tipo de retorno de items de StateFlow<List<Article>> para Flow<PagingData<Article>>. Para fazer isso, primeiro adicione uma constante particular com o nome ITEMS_PER_PAGE na parte de cima do arquivo:

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel {
    ...
}

Em seguida, vamos atualizar items para ser o resultado da saída de uma instância de Pager. Para fazer isso, transmita dois parâmetros ao Pager:

  • Um PagingConfig com um pageSize de ITEMS_PER_PAGE e marcadores de posição desativados.
  • Uma PagingSourceFactory que fornece uma instância da ArticlePagingSource que acabamos de criar.
class ArticleViewModel(...) : ViewModel() {

   val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        ...
}

Em seguida, para manter o estado de paginação com mudanças de configuração ou navegação, usamos o método cachedIn() transmitindo o androidx.lifecycle.viewModelScope.

Depois de concluir as mudanças acima, o ViewModel vai ficar assim:

package com.example.android.codelabs.paging.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.android.codelabs.paging.data.Article
import com.example.android.codelabs.paging.data.ArticleRepository
import com.example.android.codelabs.paging.data.ITEMS_PER_PAGE
import kotlinx.coroutines.flow.Flow

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel(
    private val repository: ArticleRepository,
) : ViewModel() {

    val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        .cachedIn(viewModelScope)
}

Outro fator a ser observado sobre PagingData é que esse é um tipo independente que contém um fluxo mutável de atualizações para os dados que são exibidos na RecyclerView. Cada emissão de PagingData é completamente independente, e várias instâncias de PagingData podem ser emitidas para uma única consulta se a PagingSource de suporte for invalidada devido a mudanças no conjunto de dados. Dessa forma, os Flows de PagingData precisam ser exposto independente de outros Flows.

Pronto! Agora temos a função de paginação no ViewModel.

8. Fazer o adaptador funcionar com PagingData

Para vincular PagingData a uma RecyclerView, use um PagingDataAdapter. O PagingDataAdapter é notificado sempre que o conteúdo do PagingData é carregado e, então, sinaliza ao RecyclerView que é necessário atualizar.

Atualizar o ArticleAdapter para funcionar com um fluxo PagingData

  • No momento, o ArticleAdapter implementa o ListAdapter. Em vez disso, faça com que ele implemente o PagingDataAdapter. O restante do corpo da classe permanecerá inalterado:
import androidx.paging.PagingDataAdapter
...

class ArticleAdapter : PagingDataAdapter<Article, RepoViewHolder>(ARTICLE_DIFF_CALLBACK) {
// body is unchanged
}

Fizemos muitas mudanças até aqui, e agora falta pouco para executar o app. Basta conectar a IU.

9. Consumir PagingData na IU

Na implementação atual, temos um método com o nome binding.setupScrollListener() que chama o ViewModel para carregar mais dados caso certas condições sejam atendidas. A biblioteca Paging faz tudo isso automaticamente. Assim, podemos excluir esse método e os usos dele.

Em seguida, como o ArticleAdapter não é mais um ListAdapter, mas sim um PagingDataAdapter, faremos duas pequenas mudanças:

  • Vamos mudar o operador de terminal no Flow do ViewModel para collectLatest, em vez de collect.
  • Vamos notificar o ArticleAdapter sobre mudanças com submitData() em vez de submitList().

Usamos collectLatest no pagingData Flow para cancelar a coleta das emissões de pagingData anteriores quando uma nova instância de pagingData for emitida.

Com essas mudanças, a Activity vai ficar assim:

import kotlinx.coroutines.flow.collectLatest

class ArticleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityArticlesBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        val viewModel by viewModels<ArticleViewModel>(
            factoryProducer = { Injection.provideViewModelFactory(owner = this) }
        )

        val items = viewModel.items
        val articleAdapter = ArticleAdapter()

        binding.bindAdapter(articleAdapter = articleAdapter)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                items.collectLatest {
                    articleAdapter.submitData(it)
                }
            }
        }
    }
}

private fun ActivityArticlesBinding.bindAdapter(
    articleAdapter: ArticleAdapter
) {
    list.adapter = articleAdapter
    list.layoutManager = LinearLayoutManager(list.context)
    val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
    list.addItemDecoration(decoration)
}

Agora, o app vai ser compilado e executado. Você migrou o app para a biblioteca Paging.

f97136863cfa19a0.gif

10. Mostrar estados de carregamento na IU

Quando a biblioteca Paging busca mais itens para exibir na IU, a prática recomendada é indicar ao usuário que há mais dados a caminho. Felizmente, a biblioteca Paging oferece uma maneira fácil de acessar o status de carregamento com o tipo CombinedLoadStates.

As instâncias de CombinedLoadStates descrevem o status de carregamento de todos os componentes da biblioteca Paging que carregam dados. Em nosso caso, estamos interessados no LoadState apenas da ArticlePagingSource, então vamos trabalhar principalmente com o tipo LoadStates no campo CombinedLoadStates.source. Você pode acessar CombinedLoadStates pelo PagingDataAdapter via PagingDataAdapter.loadStateFlow.

CombinedLoadStates.source é um tipo de LoadStates, com campos para três tipos diferentes de LoadState:

  • LoadStates.append: para o LoadState de itens buscados depois da posição atual do usuário.
  • LoadStates.prepend: para o LoadState de itens buscados antes da posição atual do usuário.
  • LoadStates.refresh: para o LoadState do carregamento inicial.

Cada LoadState pode ser um destes tipos:

  • LoadState.Loading: os itens estão sendo carregados.
  • LoadState.NotLoading: os itens não estão sendo carregados.
  • LoadState.Error: ocorreu um erro de carregamento.

No nosso caso, só vamos nos preocupar se o LoadState for LoadState.Loading porque nossa ArticlePagingSource não inclui um caso de erro.

A primeira coisa que vamos fazer é adicionar barras de progresso às partes de cima e de baixo da IU para indicar o status de carregamento das buscas em qualquer direção.

Em activity_articles.xml, adicione duas barras LinearProgressIndicator desta maneira:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.ArticleActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/prepend_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/append_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Em seguida, reagimos ao CombinedLoadState coletando o LoadStatesFlow do PagingDataAdapter. Colete o estado em ArticleActivity.kt:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                articleAdapter.loadStateFlow.collect {
                    binding.prependProgress.isVisible = it.source.prepend is Loading
                    binding.appendProgress.isVisible = it.source.append is Loading
                }
            }
        }
        lifecycleScope.launch {
        ...
    }

Por fim, vamos adicionar um pequeno atraso à ArticlePagingSource para simular o carregamento:

private const val LOAD_DELAY_MILLIS = 3_000L

class ArticlePagingSource : PagingSource<Int, Article>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        val start = params.key ?: STARTING_KEY
        val range = startKey.until(startKey + params.loadSize)

        if (start != STARTING_KEY) delay(LOAD_DELAY_MILLIS)
        return ...

}

Execute o app novamente e role até a parte de baixo da lista. Você vai ver a barra de progresso na parte de baixo enquanto a biblioteca Paging buscar mais itens. A barra desaparece quando a busca é concluída.

6277154193f7580.gif

11. Conclusão

Vamos fazer um resumo rápido do que vimos. Nós:

  • Exploramos uma visão geral da paginação e por que ela é necessária.
  • Adicionamos a paginação ao app criando um Pager, definindo uma PagingSource e emitindo PagingData.
  • Armazenamos PagingData em cache no ViewModel usando o operador cachedIn.
  • Consumimos PagingData na IU usando um PagingDataAdapter.
  • Reagimos a CombinedLoadStates usando PagingDataAdapter.loadStateFlow.

Pronto! Para ver conceitos de paginação mais aprofundados, confira o codelabavançado da Paging.