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:
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:
O que é necessário
Opcional
- Familiaridade com estes componentes da arquitetura: ViewModel, Vinculação de visualizações e com a arquitetura sugerida no Guia para a arquitetura do app. Para uma introdução aos componentes da arquitetura, consulte o codelab Room com View.
- Familiaridade com corrotinas e fluxo (em inglês) em Kotlin. Para ver uma introdução ao fluxo, confira o codelab Corrotinas avançadas com fluxo do Kotlin e LiveData.
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
.
- Abra o projeto no diretório
basic/start
no Android Studio. - Execute a configuração de execução
app
em um dispositivo ou emulador.
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 performance, já que 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 classeDataSource
e, depois, peloRepository
para uso noViewModel
.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 dePagingData
. Ela depende daPagingSource
para fazer isso e precisa ser criada noViewModel
.PagingData
: contêiner para dados paginados. Cada atualização de dados tem uma emissão dePagingData
separada correspondente, com suporte da própriaPagingSource
.PagingDataAdapter
: uma subclasse deRecyclerView.Adapter
que mostraPagingData
em umaRecyclerView
. OPagingDataAdapter
pode ser conectado a umFlow
do Kotlin, umLiveData
, umFlowable
do RxJava, umObservable
do RxJava ou até mesmo a uma lista estática usando métodos de fábrica. OPagingDataAdapter
detecta eventos de carregamento internos dePagingData
e atualiza a IU de forma eficiente à medida que as páginas são carregadas.
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
eRecyclerView.ViewHolder
: classes responsáveis por exibir a lista na IU.ViewModel
: o holder do estado responsável por criar o estado que a interface precisa mostrar.
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 noitems
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. No 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 sernull
. 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 vamos adicionar uma constanteSTARTING_KEY
de0
à parte de cima do arquivoArticlePagingSource
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 aPagingSource
for invalidada porque não pode mais garantir a integridade dos resultados.
Uma LoadResult.Page
tem três argumentos obrigatórios:
data
: umaList
dos itens buscados.prevKey
: a chave usada pelo métodoload()
se ele precisar buscar itens antes da página atual.nextKey
: a chave usada pelo métodoload()
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 aSTARTING_KEY
, vamos retornar 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 queSTARTING_KEY
. Para isso, definimos o métodoensureValidKey()
.
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()
noPagingAdapter
. - Você chamou
invalidate()
naPagingSource
.
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 vai saber que precisa buscá-los 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, garantindo 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 envolve 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: usePager.flow
.LiveData
: usePager.liveData
.Flowable
RxJava: usePager.flowable
.Observable
RxJava: usePager.observable
.
Como já estamos usando o Flow
no app, vamos continuar 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 umaPagingSource
. 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âmetromaxSize
emPagingConfig
. 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çãoenablePlaceholders
fortrue
. 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 transmitindoenablePlaceholders = false
.- Uma função que define como criar a
PagingSource
. No nosso caso, vamos criar umaArticlePagingSource
, 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 aArticlePagingSource
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 umpageSize
deITEMS_PER_PAGE
e marcadores de posição desativados. - Uma
PagingSourceFactory
que fornece uma instância daArticlePagingSource
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 oListAdapter
. Em vez disso, faça com que ele implemente oPagingDataAdapter
. 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
doViewModel
paracollectLatest
, em vez decollect
. - Vamos notificar o
ArticleAdapter
sobre mudanças comsubmitData()
em vez desubmitList()
.
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.
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. No 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 oLoadState
de itens buscados depois da posição atual do usuário.LoadStates.prepend
: para oLoadState
de itens buscados antes da posição atual do usuário.LoadStates.refresh
: para oLoadState
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 busca mais itens. A barra desaparece quando a busca é concluída.
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 umaPagingSource
e emitindoPagingData
. - Armazenamos
PagingData
em cache noViewModel
usando o operadorcachedIn
. - Consumimos
PagingData
na IU usando umPagingDataAdapter
. - Reagimos a
CombinedLoadStates
usandoPagingDataAdapter.loadStateFlow
.
Pronto! Para ver conceitos de paginação mais aprofundados, confira o codelabavançado da Paging.