Gerenciar e exibir estados de carregamento

A biblioteca Paging monitora o estado das solicitações de carregamento dos dados paginados e os expõe usando a classe LoadState. O app pode registrar um listener usando o PagingDataAdapter para receber informações sobre o estado atual e atualizar a IU da forma adequada. Esses estados são fornecidos pelo adaptador porque são síncronos com a IU. Isso significa que o listener recebe atualizações quando o carregamento da página é aplicado à IU.

Um sinal LoadState separado é fornecido para cada LoadType e cada tipo de fonte de dados (PagingSource ou RemoteMediator). O objeto CombinedLoadStates fornecido pelo listener fornece informações sobre o estado de carregamento de todos esses sinais. Use essas informações detalhadas para exibir os indicadores de carregamento adequados aos usuários.

Estados de carregamento

A biblioteca Paging usa o objeto LoadState para expor o estado de carregamento que será usado na IU. Os objetos LoadState podem assumir uma de três formas, dependendo do estado de carregamento atual:

  • Se não houver operação de carregamento ativa nem erros, LoadState será um objeto LoadState.NotLoading. Essa subclasse também inclui a propriedade endOfPaginationReached, que indica se o fim da paginação foi atingido.
  • Se houver uma operação de carregamento ativa, LoadState será um objeto LoadState.Loading.
  • Se houver algum erro, LoadState será um objeto LoadState.Error.

Há duas maneiras de usar LoadState na IU: usar um listener ou um adaptador de lista especial para apresentar o estado de carregamento diretamente na lista RecyclerView.

Acessar o estado de carregamento usando um listener

Para acessar o estado de carregamento para uso geral na IU, use o fluxo loadStateFlow ou o método addLoadStateListener() fornecido pelo PagingDataAdapter. Esses mecanismos fornecem acesso a um objeto CombinedLoadStates que inclui informações sobre o comportamento de LoadState para cada tipo de carregamento.

No exemplo a seguir, o PagingDataAdapter exibe componentes de IU diferentes, dependendo do estado atual do carregamento de atualização:

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

Para mais informações sobre CombinedLoadStates, consulte Acessar outras informações sobre o estado de carregamento.

Exibir o estado de carregamento usando um adaptador

A biblioteca Paging oferece outro adaptador de lista chamado LoadStateAdapter para apresentar o estado de carregamento diretamente na lista exibida de dados paginados. Esse adaptador fornece acesso ao estado de carregamento atual da lista, que pode ser transmitido para um armazenador de visualização personalizado que exibe as informações.

Primeiro, crie uma classe para armazenar visualizações que mantém referências às visualizações de carregamento e de erro na tela. Crie uma função bind() que aceite um LoadState como parâmetro. Essa função precisa alternar a visibilidade da visualização com base no parâmetro do estado de carregamento:

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

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

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

Em seguida, crie uma classe que implemente LoadStateAdapter e defina os métodos onCreateViewHolder() e onBindViewHolder(). Esses métodos criam uma instância do armazenador de visualização personalizado e vinculam o estado de carregamento associado.

Kotlin

// Adapter that displays a loading spinner when
// state is 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

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is 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

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is 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);
  }
}

Para exibir o progresso do carregamento em um cabeçalho e rodapé, 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ê pode chamar withLoadStateHeader() ou withLoadStateFooter() se quiser que a lista RecyclerView exiba o estado de carregamento apenas no cabeçalho ou no rodapé.

Acessar informações adicionais sobre o estado de carregamento

O objeto CombinedLoadStates do PagingDataAdapter fornece informações sobre os estados de carregamento para a implementação de PagingSource e também para a implementação de RemoteMediator, se houver.

Por conveniência, é possível usar as propriedades refresh, append e prepend de CombinedLoadStates para acessar um objeto LoadState do tipo de carregamento específico. Geralmente, essas propriedades são vinculadas ao estado de carregamento da implementação de RemoteMediator, se houver. Caso contrário, eles contêm o estado de carregamento da implementação PagingSource. Para ver informações mais detalhadas sobre essa lógica, consulte a documentação de referência para CombinedLoadStates.

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Observe refresh load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    refreshLoadState: LoadState = loadStates.refresh
    // Observe prepend load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    prependLoadState: LoadState = loadStates.prepend
    // Observe append load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    appendLoadState: LoadState = loadStates.append
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

No entanto, é importante lembrar que apenas os estados de carregamento PagingSource têm a garantia de serem síncronos com atualizações da IU. Como as propriedades refresh, append e prepend podem potencialmente receber o estado de carregamento de PagingSource ou RemoteMediator, não há garantia de que elas sejam síncronas com atualizações da interface. Isso pode causar problemas de IU em que o carregamento parece terminar antes de qualquer dado novo ser adicionado a ela.

Por esse motivo, os acessadores de conveniência funcionam bem para exibir o estado de carregamento em um cabeçalho ou rodapé, mas, para outros casos de uso, pode ser necessário acessar especificamente o estado de carregamento de PagingSource ou RemoteMediator. CombinedLoadStates fornece as propriedades source e mediator para essa finalidade. Cada uma dessas propriedades expõe um objeto LoadStates que contém os objetos LoadState de PagingSource ou RemoteMediator, respectivamente:

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Directly access the RemoteMediator refresh load state.
    mediatorRefreshLoadState: LoadState? = loadStates.mediator.refresh
    // Directly access the RemoteMediator append load state.
    mediatorAppendLoadState: LoadState? = loadStates.mediator.append
    // Directly access the RemoteMediator prepend load state.
    mediatorPrependLoadState: LoadState? = loadStates.mediator.prepend
    // Directly access the PagingSource refresh load state.
    sourceRefreshLoadState: LoadState = loadStates.source.refresh
    // Directly access the PagingSource append load state.
    sourceAppendLoadState: LoadState = loadStates.source.append
    // Directly access the PagingSource prepend load state.
    sourcePrependLoadState: LoadState = loadStates.source.prepend
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Operadores de cadeia em LoadState

Como o objeto CombinedLoadStates fornece acesso a todas as mudanças no estado de carregamento, é importante filtrar o fluxo do estado de carregamento com base em eventos específicos. Isso garante que você atualize a IU no momento apropriado para evitar a renderização lenta e atualizações desnecessárias.

Por exemplo, suponha que você queira exibir uma visualização vazia, mas somente depois que o carregamento inicial de dados for concluído. Neste caso de uso, é necessário verificar se um carregamento de atualização de dados foi iniciado e aguardar o estado NotLoading confirmar a conclusão. Filtre todos os sinais, exceto os necessários:

Kotlin

lifecycleScope.launchWhenCreated {
  adapter.loadStateFlow
    // Only emit when REFRESH LoadState for RemoteMediator changes.
    .distinctUntilChangedBy { it.refresh }
    // Only react to cases where REFRESH completes, such as NotLoading.
    .filter { it.refresh is LoadState.NotLoading }
    // Scroll to top is synchronous with UI updates, even if remote load was
    // triggered.
    .collect { binding.list.scrollToPosition(0) }
}

Java

PublishSubject<CombinedLoadStates> subject = PublishSubject.create();
Disposable disposable =
  subject.distinctUntilChanged(CombinedLoadStates::getRefresh)
  .filter(
    combinedLoadStates -> combinedLoadStates.getRefresh() instanceof LoadState.NotLoading)
  .subscribe(combinedLoadStates -> binding.list.scrollToPosition(0));

pagingAdapter.addLoadStateListener(loadStates -> {
  subject.onNext(loadStates);
});

Java

LiveData<CombinedLoadStates> liveData = new MutableLiveData<>();
LiveData<LoadState> refreshLiveData =
  Transformations.map(liveData, CombinedLoadStates::getRefresh);
LiveData<LoadState> distinctLiveData =
  Transformations.distinctUntilChanged(refreshLiveData);

distinctLiveData.observeForever(loadState -> {
  if (loadState instanceof LoadState.NotLoading) {
    binding.list.scrollToPosition(0);
  }
});

Este exemplo aguarda até que o estado de carregamento de atualização seja atualizado, mas só é acionado quando o estado é NotLoading. Isso garante que a atualização remota seja totalmente concluída antes que qualquer atualização de IU aconteça.

As APIs de fluxo tornam esse tipo de operação possível. O app pode especificar os eventos de carregamento necessários e processar os novos dados quando os critérios apropriados forem atendidos.