Zarządzaj stanami wczytywania i wyświetlaj je

Biblioteka stronicowania śledzi stan żądań wczytania danych z podziałem na strony i udostępnia je za pomocą klasy LoadState. Aplikacja może zarejestrować detektor w elemencie PagingDataAdapter, aby otrzymywać informacje o bieżącym stanie i odpowiednio aktualizować interfejs. Te stany są dostarczane z adaptera, ponieważ są synchroniczne z interfejsem użytkownika. Oznacza to, że detektor będzie otrzymywać aktualizacje po zastosowaniu w interfejsie wczytania strony.

Dla każdego parametru LoadType i typu źródła danych (PagingSource lub RemoteMediator) jest dostarczany oddzielny sygnał LoadState. Obiekt CombinedLoadStates dostarczany przez detektor dostarcza informacji o stanie wczytywania ze wszystkich tych sygnałów. Na podstawie tych szczegółowych informacji możesz wyświetlać użytkownikom odpowiednie wskaźniki wczytywania.

Wczytuję stany

Biblioteka stronicowania ujawnia stan wczytywania do użycia w interfejsie za pomocą obiektu LoadState. Obiekty LoadState przyjmują 1 z 3 postaci w zależności od bieżącego stanu wczytywania:

  • Jeśli nie ma żadnej aktywnej operacji wczytywania ani błędu, LoadState jest obiektem LoadState.NotLoading. Ta podklasa zawiera też właściwość endOfPaginationReached, która wskazuje, czy został osiągnięty koniec podziału na strony.
  • Jeśli występuje aktywna operacja wczytywania, LoadState jest obiektem LoadState.Loading.
  • Jeśli wystąpi błąd, LoadState jest obiektem LoadState.Error.

Z dyrektywy LoadState można korzystać w interfejsie na 2 sposoby: za pomocą odbiornika lub specjalnego adaptera listy, aby prezentować stan wczytywania bezpośrednio na liście RecyclerView.

Uzyskiwanie dostępu do stanu wczytywania za pomocą detektora

Aby uzyskać stan wczytywania do ogólnego użytku w interfejsie, użyj strumienia loadStateFlow lub metody addLoadStateListener() podanej przez PagingDataAdapter. Mechanizmy te zapewniają dostęp do obiektu CombinedLoadStates, który zawiera informacje o zachowaniu LoadState w poszczególnych typach wczytywania.

W tym przykładzie PagingDataAdapter wyświetla różne komponenty UI w zależności od bieżącego stanu wczytywania odświeżania:

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

Więcej informacji o właściwości CombinedLoadStates znajdziesz w artykule Uzyskiwanie dostępu do dodatkowych informacji o stanie wczytywania.

Prezentowanie stanu wczytywania za pomocą adaptera

Biblioteka stronicowania zawiera kolejny adapter listy o nazwie LoadStateAdapter, który umożliwia prezentowanie stanu wczytywania bezpośrednio na wyświetlanej liście danych z podziałem na strony. Zapewnia on dostęp do bieżącego stanu wczytywania listy, który możesz przekazać osobie odpowiedzialnej za widok niestandardowy, która wyświetla te informacje.

Najpierw utwórz klasę widoku danych, która zachowuje odwołania do widoków wczytywania i błędów na ekranie. Utwórz funkcję bind(), która akceptuje LoadState jako parametr. Ta funkcja powinna przełączać widoczność widoku w zależności od parametru stanu wczytywania:

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

Następnie utwórz klasę, która implementuje LoadStateAdapter, i zdefiniuj metody onCreateViewHolder() oraz onBindViewHolder(). Te metody tworzą instancję niestandardowego właściciela widoku i wiążą powiązany stan wczytywania.

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

Aby wyświetlić postęp wczytywania w nagłówku i stopce, wywołaj metodę withLoadStateHeaderAndFooter() z obiektu 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));

Możesz zamiast tego wywołać metodę withLoadStateHeader() lub withLoadStateFooter(), jeśli chcesz, by lista RecyclerView wyświetlała stan wczytywania tylko w nagłówku lub tylko w stopce.

Dostęp do dodatkowych informacji o stanie wczytywania

Obiekt CombinedLoadStates z PagingDataAdapter zawiera informacje o stanach obciążenia Twojej implementacji PagingSource oraz RemoteMediator, jeśli taka istnieje.

Dla wygody możesz użyć właściwości refresh, append i prepend z CombinedLoadStates, aby uzyskać dostęp do obiektu LoadState odpowiedniego typu wczytywania. Jeśli taka właściwość istnieje, właściwości te zazwyczaj przejmują stan wczytywania z implementacji RemoteMediator. Jeśli nie, zawierają odpowiedni stan wczytywania z implementacji PagingSource. Więcej informacji o podstawowej logice znajdziesz w dokumentacji referencyjnej 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;
});

Pamiętaj jednak, że tylko stany wczytywania PagingSource będą synchroniczne wraz z aktualizacjami interfejsu. Usługi refresh, append i prepend mogą potencjalnie przejąć stan wczytywania z PagingSource lub RemoteMediator, dlatego nie ma gwarancji, że będą one synchronizowane z aktualizacjami interfejsu. Może to powodować problemy z interfejsem – wczytywanie powinno zakończyć się przed dodaniem nowych danych do interfejsu.

Z tego względu wygodne metody dostępu dobrze sprawdzają się do wyświetlania stanu wczytywania w nagłówku lub stopce, ale w innych przypadkach dostęp do stanu wczytywania może mieć konkretnie PagingSource lub RemoteMediator. CombinedLoadStates udostępnia do tego celu właściwości source i mediator. Każda z tych właściwości ujawnia obiekt LoadStates, który zawiera obiekty LoadState dla PagingSource lub RemoteMediator:

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

Operatory łańcucha w LoadState

Obiekt CombinedLoadStates zapewnia dostęp do wszystkich zmian stanu obciążenia, dlatego ważne jest, aby filtrować strumień stanu obciążenia według konkretnych zdarzeń. Dzięki temu interfejs użytkownika aktualizuje się w odpowiednim momencie, co pozwala uniknąć zacinania się i niepotrzebnych aktualizacji.

Załóżmy na przykład, że chcesz wyświetlić pusty widok, ale dopiero po zakończeniu wstępnego wczytywania danych. Ten przypadek użycia wymaga sprawdzenia, czy rozpoczęło się odświeżanie danych, a następnie poczekać, aż odświeżenie za pomocą stanu NotLoading potwierdzi, że się ono zakończyło. Musisz odfiltrować wszystkie sygnały oprócz tych, które są Ci potrzebne:

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

Ten przykład czeka na aktualizację stanu wczytywania odświeżania, ale jest wywoływany tylko wtedy, gdy stan to NotLoading. Dzięki temu zdalne odświeżanie zostanie całkowicie zakończone, zanim interfejs użytkownika zostanie zaktualizowany.

Interfejsy API strumieniowania umożliwiają taki rodzaj operacji. Aplikacja może określić wymagane zdarzenia wczytywania i obsługiwać nowe dane, gdy zostaną spełnione odpowiednie kryteria.