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 lazy.

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 um 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.

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.
  • Se o carregamento não for bem-sucedido, um objeto LoadResult.Error é retornado.
  • Se o PagingSource não for mais válido e precisar ser substituído por uma nova instância (por exemplo, devido a uma mudança nos dados subjacentes), 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.

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 Pager como ter uma instância da implementação de PagingSource, conforme mostrado no exemplo a seguir.

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

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        // Configure how data is loaded by passing additional properties to
        // PagingConfig, such as pageSize and enabling or disabling placeholders.
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = true
        ),
        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, não é possível coletar o PagingData. 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, extraia o fluxo do seu ViewModel e transmita-o ao elemento combinável da 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 criar o layout de 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 ele 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ê ativou os marcadores, precisa 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