Zarządzanie stanami wczytywania i ich prezentowanie (widoki)

Pojęcia i implementacja w Jetpack Compose

Biblioteka Paging śledzi stan żądań wczytywania danych podzielonych na strony i udostępnia go za pomocą klasy LoadState. Aplikacja może zarejestrować detektor w PagingDataAdapter, aby otrzymywać informacje o bieżącym stanie i odpowiednio aktualizować interfejs. Te stany są dostarczane przez adapter, ponieważ są synchroniczne z interfejsem. Oznacza to, że odbiorca otrzymuje aktualizacje, gdy wczytanie strony zostanie zastosowane w interfejsie.

Dla każdego typu LoadState sygnału LoadType i źródła danych (PagingSource lub RemoteMediator) jest udostępniany osobny sygnał. Obiekt CombinedLoadStates udostępniany przez odbiornik zawiera informacje o stanie wczytywania ze wszystkich tych sygnałów. Możesz użyć tych szczegółowych informacji, aby wyświetlać użytkownikom odpowiednie wskaźniki wczytywania.

Wczytuję stany

Biblioteka Paging udostępnia stan wczytywania do użycia w interfejsie za pomocą obiektu LoadState. LoadState obiekty przyjmują jedną z 3 form w zależności od bieżącego stanu wczytywania:

  • Jeśli nie ma aktywnej operacji wczytywania i nie wystąpił żaden błąd, LoadState jest obiektem LoadState.NotLoading. Ta podklasa zawiera też właściwość endOfPaginationReached, która wskazuje, czy osiągnięto koniec paginacji.
  • Jeśli trwa aktywna operacja wczytywania, LoadState jest obiektem LoadState.Loading.
  • Jeśli wystąpi błąd, LoadState jest obiektem LoadState.Error.

W interfejsie możesz używać LoadState na 2 sposoby: za pomocą odbiornika lub specjalnego adaptera listy, aby wyświetlać stan ładowania bezpośrednio na liście RecyclerView.

Dostęp do stanu wczytywania za pomocą detektora

Aby uzyskać stan wczytywania do ogólnego użytku w interfejsie, użyj strumienia loadStateFlow lub metody addLoadStateListener() udostępnianej przez PagingDataAdapter. Te mechanizmy zapewniają dostęp do obiektu CombinedLoadStates, który zawiera informacje o LoadState zachowaniu w przypadku każdego typu wczytywania.

W poniższym przykładzie element PagingDataAdapter wyświetla różne komponenty interfejsu w zależności od bieżącego stanu ładowania 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 CombinedLoadStates znajdziesz w sekcji Dostęp do dodatkowych informacji o stanie wczytywania.

Wyświetlanie stanu wczytywania za pomocą komponentu adaptera

Biblioteka Paging udostępnia kolejny adapter listy o nazwie LoadStateAdapter, który służy do prezentowania stanu ładowania bezpośrednio na wyświetlanej liście danych podzielonych na strony. Ten adapter zapewnia dostęp do bieżącego stanu wczytywania listy, który możesz przekazać do niestandardowego obiektu ViewHolder wyświetlającego informacje.

Najpierw utwórz klasę uchwytu widoku, która będzie przechowywać odwołania do widoków ładowania i błędów na ekranie. Utwórz funkcję bind(), która akceptuje LoadState jako parametr. Ta funkcja powinna przełączać widoczność widoku na podstawie parametru loadState:

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 interfejs LoadStateAdapter, i zdefiniuj metody onCreateViewHolder()onBindViewHolder(). Te metody tworzą instancję niestandardowego uchwytu widoku i wiążą z nią 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));

Zamiast tego możesz wywołać funkcję withLoadStateHeader() lub withLoadStateFooter() , jeśli chcesz, aby lista RecyclerView wyświetlała stan ładowania tylko w nagłówku lub tylko w stopce.

Dostęp do dodatkowych informacji o stanie wczytywania

Obiekt CombinedLoadStatesPagingDataAdapter zawiera informacje o stanach wczytywania w przypadku implementacji PagingSource, a także implementacji RemoteMediator, jeśli taka istnieje.

Aby ułatwić sobie pracę, możesz użyć właściwości refresh, appendprependCombinedLoadStates, aby uzyskać dostęp do obiektu LoadState dla odpowiedniego typu wczytywania. Te właściwości zwykle odwołują się do stanu wczytywania z implementacji RemoteMediator, jeśli taka istnieje. W przeciwnym razie zawierają odpowiedni stan wczytywania z implementacji PagingSource. Szczegółowe informacje o logice działania znajdziesz w dokumentacji referencyjnej dotyczącej 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 PagingSourcestany wczytywaniaPagingSource są synchronizowane z aktualizacjami interfejsu. Ponieważ właściwości refresh, appendprepend mogą potencjalnie przyjmować stan wczytywania z PagingSource lub RemoteMediator, nie gwarantuje się, że będą one zsynchronizowane z aktualizacjami interfejsu. Może to powodować problemy z interfejsem, w którym ładowanie wydaje się kończyć, zanim do interfejsu zostaną dodane nowe dane.

Z tego powodu wygodne akcesory dobrze sprawdzają się do wyświetlania stanu ładowania w nagłówku lub stopce, ale w innych przypadkach może być konieczne uzyskanie dostępu do stanu ładowania z PagingSource lub RemoteMediator. CombinedLoadStates udostępnia w tym celu właściwości sourcemediator. Każda z tych właściwości udostępnia 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;
});

Operatorzy łańcuchowi w LoadState

Ponieważ obiekt CombinedLoadStates zapewnia dostęp do wszystkich zmian stanu wczytywania, ważne jest, aby filtrować strumień stanu wczytywania na podstawie konkretnych zdarzeń. Dzięki temu zaktualizujesz interfejs w odpowiednim momencie, aby uniknąć zacięć i niepotrzebnych aktualizacji.

Załóżmy na przykład, że chcesz wyświetlić pusty widok, ale dopiero po zakończeniu początkowego wczytywania danych. Ten przypadek użycia wymaga sprawdzenia, czy rozpoczęło się odświeżanie danych, a następnie poczekania na stan NotLoading, aby potwierdzić zakończenie odświeżania. Musisz odfiltrować wszystkie sygnały z wyjątkiem tych, których potrzebujesz:

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

W tym przykładzie oczekujemy na zaktualizowanie stanu wczytywania odświeżania, ale wywołujemy go tylko wtedy, gdy stan to NotLoading. Dzięki temu zdalne odświeżanie zostanie w pełni zakończone przed wprowadzeniem jakichkolwiek zmian w interfejsie.

Interfejsy Stream API umożliwiają tego typu operacje. Aplikacja może określać zdarzenia wczytywania, których potrzebuje, i obsługiwać nowe dane, gdy zostaną spełnione odpowiednie kryteria.