A biblioteca Paging fornece recursos avançados para carregar e exibir
dados paginados de um conjunto de dados maior. Este guia demonstra como usar a biblioteca Paging
para configurar um fluxo de dados paginados de uma fonte de dados de rede e
exibi-los em uma RecyclerView
.
Definir uma fonte de dados
O primeiro passo é definir uma implementação de
PagingSource
para identificar a fonte de dados. A classe de API PagingSource
inclui o método
load()
,
que precisa ser modificado para indicar como recuperar dados paginados da
fonte de dados correspondente.
Use a classe PagingSource
diretamente para usar corrotinas do Kotlin para
carregamento assíncrono. A biblioteca Paging também fornece classes para compatibilidade com outros
frameworks assíncronos:
- Para usar RxJava, implemente
RxPagingSource
. - Para usar
ListenableFuture
do Guava, implementeListenableFuturePagingSource
.
Selecionar tipos de chave e valor
PagingSource<Key, Value>
tem dois parâmetros de tipo: Key
e Value
. A chave
define o identificador usado para carregar os dados, e o valor é o tipo dos
próprios dados. Por exemplo, se você carregar páginas de objetos User
da rede
transmitindo números de página Int
para
Retrofit,
selecione Int
como o tipo Key
e User
como o tipo Value
.
Definir a PagingSource
O exemplo a seguir implementa uma
PagingSource
que carrega
páginas de itens por número de página. O tipo Key
é Int
, e o tipo Value
é
User
.
Kotlin
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { try { // Start refresh at page 1 if undefined. val nextPageNumber = params.key ?: 1 val response = backend.searchUsers(query, nextPageNumber) return LoadResult.Page( data = response.users, prevKey = null, // Only paging forward. nextKey = response.nextPageNumber ) } catch (e: Exception) { // Handle errors in this block and return LoadResult.Error if it is an // expected error (such as a network failure). } } }
Java
class ExamplePagingSource extends RxPagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; ExamplePagingSource(@NonNull ExampleBackendService backend, @NonNull String query) { mBackend = backend; mQuery = query; } @NotNull @Override public Single<LoadResult<Integer, User>> loadSingle( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } return mBackend.searchUsers(mQuery, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUsersResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } }
Java
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; @NonNull private Executor mBgExecutor; ExamplePagingSource( @NonNull ExampleBackendService backend, @NonNull String query, @NonNull Executor bgExecutor) { mBackend = backend; mQuery = query; mBgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<LoadResult<Integer, User>> loadFuture( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( mBackend.searchUsers(mQuery, nextPageNumber), this::toLoadResult, mBgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, mBgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, mBgExecutor); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUsersResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } }
Uma implementação típica de PagingSource
transmite parâmetros fornecidos no
construtor ao método load()
para carregar os dados apropriados em uma consulta. No
exemplo acima, esses parâmetros são:
backend
: uma instância do serviço de back-end que fornece os dados.query
: a consulta de pesquisa a ser enviada ao serviço indicado porbackend
.
O objeto LoadParams
contém informações sobre a operação de carregamento a ser executada. Isso
inclui a chave e o número de itens a serem carregados.
O objeto LoadResult
contém o resultado da operação de carregamento. LoadResult
é uma classe selada
que pode assumir uma de duas formas, dependendo do êxito da chamada load()
:
- Se o carregamento for bem-sucedido, um objeto
LoadResult.Page
é retornado. - Se o carregamento não for bem-sucedido, um objeto
LoadResult.Error
é retornado.
A Figura 1 ilustra como a função load()
neste exemplo recebe a chave
para cada carregamento e fornece a chave para o carregamento subsequente.
load()
usa e atualiza a
chave.
Solucionar erros
As solicitações para carregar dados podem falhar por diversos motivos, especialmente no carregamento
pela rede. Informe erros encontrados durante o carregamento retornando um
objeto LoadResult.Error
do método load()
.
Por exemplo, é possível identificar e relatar erros de carregamento em ExamplePagingSource
do exemplo anterior adicionando o seguinte ao método load()
:
Kotlin
catch (e: IOException) { // IOException for network failures. return LoadResult.Error(e) } catch (e: HttpException) { // HttpException for any non-2xx HTTP status codes. return LoadResult.Error(e) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( backend.searchUsers(query, nextPageNumber), this::toLoadResult, bgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, bgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, bgExecutor);
Para mais informações sobre como lidar com erros de Retrofit, consulte as amostras na
referência da API PagingSource
.
PagingSource
coleta e entrega objetos LoadResult.Error
à IU para
que você possa tomar medidas quanto a eles. Para saber mais como expor o estado de carregamento
na IU, consulte Exibir o estado de carregamento.
Configurar um fluxo de PagingData
Em seguida, você precisa de um fluxo de dados paginados da implementação de PagingSource
.
Normalmente, é preciso configurar o fluxo de dados no ViewModel
. A
classe Pager
oferece métodos que
expõem um fluxo reativo de
objetos PagingData
de uma
PagingSource
. A biblioteca Paging é compatível com o uso de vários tipos de fluxo,
incluindo Flow
, LiveData
e os tipos Flowable
e Observable
do
RxJava.
Ao criar uma instância de Pager
para configurar seu fluxo reativo, é necessário
fornecer à instância um objeto de configuração
PagingConfig
e uma função que informe Pager
como ter uma instância da
implementação de PagingSource
:
Kotlin
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager); PagingRx.cachedIn(flowable, viewModelScope);
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
O operador cachedIn()
torna o fluxo de dados compartilhável e armazena em cache os dados carregados
com o CoroutineScope
fornecido. Esse exemplo usa o viewModelScope
fornecido pelo artefato lifecycle-viewmodel-ktx
de Lifecycle.
O objeto Pager
chama o método load()
do objeto PagingSource
,
fornecendo o
objeto LoadParams
e recebendo o objeto
LoadResult
em troca.
Definir um adaptador RecyclerView
Também é necessário configurar um adaptador para receber os dados na sua lista
RecyclerView
. A biblioteca Paging oferece a classe PagingDataAdapter
com essa finalidade.
Defina uma classe que estenda PagingDataAdapter
. No exemplo,
UserAdapter
estende PagingDataAdapter
para fornecer um adaptador RecyclerView
para itens de lista do tipo User
e usando UserViewHolder
como proprietário
da visualização:
Kotlin
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : PagingDataAdapter<User, UserViewHolder>(diffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): UserViewHolder { return UserViewHolder(parent) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val item = getItem(position) // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item) } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Seu adaptador também precisa definir os métodos onCreateViewHolder()
e
onBindViewHolder()
e especificar um
DiffUtil.ItemCallback
.
Isso funciona da mesma maneira que a definição de adaptadores de lista
RecyclerView
:
Kotlin
object UserComparator : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // Id is unique. return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Exibir os dados paginados na sua IU
Agora que você definiu uma PagingSource
, criou uma forma para o app
gerar um fluxo de PagingData
e definiu PagingDataAdapter
, está
pronto para conectar esses elementos e exibir dados paginados em sua
atividade.
Realize as seguintes etapas no método onCreate
da atividade ou onViewCreated
do fragmento:
- Crie uma instância da classe
PagingDataAdapter
. - Transmita a instância
PagingDataAdapter
para a listaRecyclerView
em que você quer exibir os dados paginados. - Observe o fluxo
PagingData
e transmita cada valor gerado ao métodosubmitData()
do adaptador.
Kotlin
val viewModel by viewModels<ExampleViewModel>() val pagingAdapter = UserAdapter(UserComparator) val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.adapter = pagingAdapter // Activities can use lifecycleScope directly, but Fragments should instead use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } }
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter viewModel.flowable // Using AutoDispose to handle subscription lifecycle. // See: https://github.com/uber/AutoDispose .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter // Activities can use getLifecycle() directly, but Fragments should instead use // getViewLifecycleOwner().getLifecycle(). viewModel.liveData.observe(this, pagingData -> pagingAdapter.submitData(getLifecycle(), pagingData));
A lista RecyclerView
agora exibe os dados paginados da fonte de dados e
carrega automaticamente outra página quando necessário.
Exibir o estado de carregamento
A biblioteca Paging expõe o estado de carregamento a ser usado na IU por meio do
objeto LoadState
. LoadState
assume uma destas três formas, dependendo do estado de carregamento atual:
- Se não houver operação de carregamento ativa nem erros,
LoadState
será um objetoLoadState.NotLoading
. - Se houver uma operação de carregamento ativa,
LoadState
será um objetoLoadState.Loading
. - Se houver erro,
LoadState
será um objetoLoadState.Error
.
Há duas maneiras de usar LoadState
na sua IU: usar um listener e usar um
adaptador de lista especial para apresentar o estado de carregamento diretamente na
lista
RecyclerView
.
Usar um listener para apresentar o estado de carregamento
Para apresentar o estado de carregamento para uso geral na sua IU,
PagingDataAdapter
inclui o método addLoadStateListener()
.
Kotlin
// Activities can use lifecycleScope directly, but Fragments should instead use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { pagingAdapter.loadStateFlow.collectLatest { loadStates -> progressBar.isVisible = loadStates.refresh is LoadState.Loading retry.isVisible = loadState.refresh !is LoadState.Loading errorMsg.isVisible = loadState.refresh is LoadState.Error } }
Java
pagingAdapter.addLoadStateListener(loadStates -> { progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.VISIBLE : View.GONE); retry.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.GONE : View.VISIBLE); errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error ? View.VISIBLE : View.GONE); });
Java
pagingAdapter.addLoadStateListener(loadStates -> { progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.VISIBLE : View.GONE); retry.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.GONE : View.VISIBLE); errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error ? View.VISIBLE : View.GONE); });
Usar um adaptador para apresentar o estado de carregamento
A biblioteca Paging oferece outro adaptador de lista chamado LoadStateAdapter
para
apresentar o estado de carregamento diretamente na lista exibida de
dados paginados.
Primeiro, crie uma classe que implemente LoadStateAdapter
e defina os
métodos onCreateViewHolder()
e onBindViewHolder()
:
Kotlin
class LoadStateViewHolder( parent: ViewGroup, retry: () -> Unit ) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.load_state_item, parent, false) ) { private val binding = LoadStateItemBinding.bind(itemView) private val progressBar: ProgressBar = binding.progressBar private val errorMsg: TextView = binding.errorMsg private val retry: Button = binding.retryButton .also { it.setOnClickListener { retry() } } fun bind(loadState: LoadState) { if (loadState is LoadState.Error) { errorMsg.text = loadState.error.localizedMessage } progressBar.isVisible = loadState is LoadState.Loading retry.isVisible = loadState is LoadState.Error errorMsg.isVisible = loadState is LoadState.Error } } // Adapter that displays a loading spinner when // state = LoadState.Loading, and an error message and retry // button when state is LoadState.Error. class ExampleLoadStateAdapter( private val retry: () -> Unit ) : LoadStateAdapter<LoadStateViewHolder()> { override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = LoadStateViewHolder(parent, retry) override fun onBindViewHolder( holder: LoadStateViewHolder, loadState: LoadState ) = holder.bind(loadState) }
Java
class LoadStateViewHolder extends RecyclerView.ViewHolder { private ProgressBar mProgressBar; private TextView mErrorMsg; private Button mRetry; LoadStateViewHolder( @NonNull ViewGroup parent, @NonNull View.OnClickListener retryCallback) { super(LayoutInflater.from(parent.getContext()) .inflate(R.layout.load_state_item, parent, false)); LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView); mProgressBar = binding.progressBar; mErrorMsg = binding.errorMsg; mRetry = binding.retryButton; } public void bind(LoadState loadState) { if (loadState instanceof LoadState.Error) { LoadState.Error loadStateError = (LoadState.Error) loadState; mErrorMsg.setText(loadStateError.getError().getLocalizedMessage()); } mProgressBar.setVisibility(loadState instanceof LoadState.Loading ? View.VISIBLE : View.GONE); mRetry.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); mErrorMsg.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); } } // Adapter that displays a loading spinner when // state instanceOf LoadState.Loading, and an error message and // retry button when state instanceof LoadState.Error. class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> { private View.OnClickListener mRetryCallback; ExampleLoadStateAdapter(View.OnClickListener retryCallback) { mRetryCallback = retryCallback; } @NotNull @Override public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent, @NotNull LoadState loadState) { return new LoadStateViewHolder(parent, mRetryCallback); } @Override public void onBindViewHolder(@NotNull LoadStateViewHolder holder, @NotNull LoadState loadState) { holder.bind(loadState); } }
Java
class LoadStateViewHolder extends RecyclerView.ViewHolder { private ProgressBar mProgressBar; private TextView mErrorMsg; private Button mRetry; LoadStateViewHolder( @NonNull ViewGroup parent, @NonNull View.OnClickListener retryCallback) { super(LayoutInflater.from(parent.getContext()) .inflate(R.layout.load_state_item, parent, false)); LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView); mProgressBar = binding.progressBar; mErrorMsg = binding.errorMsg; mRetry = binding.retryButton; } public void bind(LoadState loadState) { if (loadState instanceof LoadState.Error) { LoadState.Error loadStateError = (LoadState.Error) loadState; mErrorMsg.setText(loadStateError.getError().getLocalizedMessage()); } mProgressBar.setVisibility(loadState instanceof LoadState.Loading ? View.VISIBLE : View.GONE); mRetry.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); mErrorMsg.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); } } // Adapter that displays a loading spinner when // state instanceOf LoadState.Loading, and an error message and // retry button when state instanceof LoadState.Error. class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> { private View.OnClickListener mRetryCallback; ExampleLoadStateAdapter(View.OnClickListener retryCallback) { mRetryCallback = retryCallback; } @NotNull @Override public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent, @NotNull LoadState loadState) { return new LoadStateViewHolder(parent, mRetryCallback); } @Override public void onBindViewHolder(@NotNull LoadStateViewHolder holder, @NotNull LoadState loadState) { holder.bind(loadState); } }
Em seguida, chame o método withLoadStateHeaderAndFooter()
do
objeto PagingDataAdapter
:
Kotlin
pagingAdapter .withLoadStateHeaderAndFooter( header = ExampleLoadStateAdapter(adapter::retry), footer = ExampleLoadStateAdapter(adapter::retry) )
Java
pagingAdapter .withLoadStateHeaderAndFooter( new ExampleLoadStateAdapter(pagingAdapter::retry), new ExampleLoadStateAdapter(pagingAdapter::retry));
Java
pagingAdapter .withLoadStateHeaderAndFooter( new ExampleLoadStateAdapter(pagingAdapter::retry), new ExampleLoadStateAdapter(pagingAdapter::retry));
Em vez disso, você poderá chamar withLoadStateHeader()
ou withLoadStateFooter()
se
quiser que apenas RecyclerView
exiba o estado de carregamento no cabeçalho ou
rodapé.
Outros recursos
Para saber mais sobre a biblioteca Paging, consulte os seguintes recursos extras: