Carregar e exibir dados paginados

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 lista lenta.

Definir uma fonte de dados

A primeira etapa é definir uma implementação da PagingSource para identificar a fonte de dados. A classe de API PagingSource inclui o método load, que é substituído para indicar como extrair dados paginados da fonte de dados correspondente.

Use a classe PagingSource diretamente para usar corrotinas do Kotlin para carregamento assíncrono.

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.

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    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 = nextPageNumber + 1
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

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 por backend

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. Para diferenciar os tipos de carregamento em load, verifique qual subclasse LoadParams foi transmitida: LoadParams.Refresh, LoadParams.Append ou LoadParams.Prepend.

O objeto LoadResult contém o resultado da operação de carregamento. LoadResult é uma classe selada que pode assumir uma de três formas, dependendo do êxito da chamada load:

  • Se o carregamento for bem-sucedido, um objeto LoadResult.Page é retornado. Transmita null para nextKey ou prevKey se não for possível carregar mais dados nessa direção.
  • Se o carregamento não for bem-sucedido, um objeto LoadResult.Error é retornado.
  • Se o PagingSource não for mais válido porque os dados subjacentes mudaram, retorne um objeto LoadResult.Invalid.

A figura abaixo ilustra como a função load neste exemplo recebe a chave para cada carregamento e para o carregamento seguinte.

Em cada chamada load, a ExamplePagingSource recebe a chave atual
    e retorna a próxima chave a ser carregada.
Figura 1. Diagrama mostrando como load usa e atualiza a chave.

A implementação de PagingSource também precisa aplicar um método getRefreshKey que receba um objeto PagingState como um parâmetro. Ele retorna a chave para transmitir ao método load quando os dados são atualizados ou invalidados após o carregamento inicial. A biblioteca Paging chama esse método automaticamente nas próximas atualizações dos dados. Para paginação baseada em deslocamento, em que as chaves representam posições de itens, é possível calcular a chave de atualização em relação a state.anchorPosition:

// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
  // Center the refreshed load around the user's current viewport so the list
  // does not jump back to the top on invalidation.
  return state.anchorPosition?.let { anchorPosition ->
    (anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
  }
}

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:

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)
}

Para mais informações sobre como lidar com erros de Retrofit, consulte as amostras na referência da API PagingSource.

PagingSource coleta e envia objetos LoadResult.Error à interface para que você possa tomar medidas quanto a eles. Para mais informações sobre como expor o estado de carregamento na interface, consulte Gerenciar e apresentar estados de carregamento.

Configurar um fluxo de PagingData

Em seguida, você precisa de um fluxo de dados paginados da implementação de PagingSource. Configure o fluxo de dados no seu ViewModel. A classe Pager oferece métodos que expõem um fluxo reativo de objetos PagingData de uma PagingSource. A biblioteca Paging expõe o fluxo de dados como um Flow.

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 ao Pager como ter uma instância da implementação de PagingSource. Também é possível especificar um initialKey opcional, como 1 para números de página baseados em 1 ou 0 para paginação baseada em deslocamento. Em PagingConfig, é possível configurar pageSize, initialLoadSize, prefetchDistance e enablePlaceholders. A propriedade prefetchDistance define quantos itens de cada borda do conteúdo carregado acionam a próxima busca.

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        config = PagingConfig(
            pageSize = 20,
            // Load 3 pages initially so the user can scroll immediately without
            // hitting a loading state.
            initialLoadSize = 60,
            // Fetch the next page far enough ahead of the scroll position to
            // avoid visible pauses.
            prefetchDistance = 20,
            enablePlaceholders = true
        ),
        initialKey = 1,
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

O operador cachedIn torna o fluxo de dados compartilhável e armazena em cache os dados carregados com o CoroutineScope fornecido. Sem cachedIn, o PagingData não pode ser coletado novamente. Esse exemplo usa o viewModelScope fornecido pelo artefato lifecycle-viewmodel-ktx do ciclo de vida.

O objeto Pager chama o método load do objeto PagingSource, fornecendo o objeto LoadParams e recebendo o objeto LoadResult em troca.

Coletar e mostrar os dados na interface

Para conectar o stream paginado à interface, acesse o fluxo do ViewModel e transmita-o para o elemento combinável de lista.

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

Use collectAsLazyPagingItems para converter o fluxo PagingData em LazyPagingItems. Em seguida, use a API items em um LazyColumn para organizar cada item.

Forneça um identificador exclusivo e estável para cada item usando itemKey. O exemplo a seguir usa it.id (referenciando a propriedade User.id) porque ela permanece estável para a instância User em todas as atualizações de dados.

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

A biblioteca Paging usa null para marcadores de posição enquanto uma página está sendo carregada. Portanto, se você tiver ativado os marcadores de posição, será necessário processar os valores null no bloco de conteúdo.

Agora, a lista mostra os dados paginados, e a biblioteca Paging carrega outras páginas conforme o usuário rola a tela.

Outros recursos

Para saber mais sobre a biblioteca Paging, consulte os seguintes recursos extras:

Documentação

Visualiza conteúdo