Управляйте и отображайте состояния загрузки

Библиотека подкачки отслеживает состояние запросов на загрузку выгружаемых данных и предоставляет его через класс LoadState . Ваше приложение может зарегистрировать прослушиватель с помощью PagingDataAdapter , чтобы получать информацию о текущем состоянии и соответствующим образом обновлять пользовательский интерфейс. Эти состояния предоставляются адаптером, поскольку они синхронны с пользовательским интерфейсом. Это означает, что ваш прослушиватель получает обновления, когда загрузка страницы применяется к пользовательскому интерфейсу.

Отдельный сигнал LoadState предоставляется для каждого LoadType и типа источника данных ( PagingSource или RemoteMediator ). Объект CombinedLoadStates , предоставляемый прослушивателем, предоставляет информацию о состоянии загрузки на основе всех этих сигналов. Эту подробную информацию можно использовать для отображения пользователям соответствующих индикаторов загрузки.

Загрузка состояний

Библиотека подкачки предоставляет состояние загрузки для использования в пользовательском интерфейсе через объект LoadState . Объекты LoadState принимают одну из трех форм в зависимости от текущего состояния загрузки:

  • Если нет активной операции загрузки и ошибок, то LoadState является объектом LoadState.NotLoading . Этот подкласс также включает свойство endOfPaginationReached , которое указывает, достигнут ли конец нумерации страниц.
  • Если есть активная операция загрузки, то LoadState является объектом LoadState.Loading .
  • Если возникла ошибка, LoadState является объектом LoadState.Error .

Существует два способа использования LoadState в пользовательском интерфейсе: использование прослушивателя или использование специального адаптера списка для представления состояния загрузки непосредственно в списке RecyclerView .

Доступ к состоянию загрузки с помощью прослушивателя

Чтобы получить состояние загрузки для общего использования в вашем пользовательском интерфейсе, используйте поток loadStateFlow или метод addLoadStateListener() , предоставленный вашим PagingDataAdapter . Эти механизмы обеспечивают доступ к объекту CombinedLoadStates , который включает информацию о поведении LoadState для каждого типа нагрузки.

В следующем примере PagingDataAdapter отображает различные компоненты пользовательского интерфейса в зависимости от текущего состояния загрузки обновления:

Котлин

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

Ява

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

Ява

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

Дополнительные сведения о CombinedLoadStates см. в разделе Доступ к дополнительной информации о состоянии загрузки .

Представление состояния загрузки с помощью адаптера

Библиотека подкачки предоставляет еще один адаптер списка, называемый LoadStateAdapter , с целью представления состояния загрузки непосредственно в отображаемом списке выгружаемых данных. Этот адаптер обеспечивает доступ к текущему состоянию загрузки списка, который вы можете передать пользовательскому держателю представления, отображающему информацию.

Сначала создайте класс держателя представления, который будет хранить ссылки на представления загрузки и ошибок на вашем экране. Создайте функцию bind() , которая принимает LoadState в качестве параметра. Эта функция должна переключать видимость представления на основе параметра состояния загрузки:

Котлин

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

Ява

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

Ява

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

Затем создайте класс, реализующий LoadStateAdapter , и определите методы onCreateViewHolder() и onBindViewHolder() . Эти методы создают экземпляр вашего пользовательского держателя представления и привязывают соответствующее состояние загрузки.

Котлин

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

Ява

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

Ява

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

Чтобы отобразить ход загрузки в верхнем и нижнем колонтитулах, вызовите метод withLoadStateHeaderAndFooter() из объекта PagingDataAdapter :

Котлин

pagingAdapter
  .withLoadStateHeaderAndFooter(
    header = ExampleLoadStateAdapter(adapter::retry),
    footer = ExampleLoadStateAdapter(adapter::retry)
  )

Ява

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Ява

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Вместо этого вы можете вызвать withLoadStateHeader() или withLoadStateFooter() если хотите, чтобы список RecyclerView отображал состояние загрузки только в заголовке или только в нижнем колонтитуле.

Доступ к дополнительной информации о состоянии загрузки

Объект CombinedLoadStates из PagingDataAdapter предоставляет информацию о состояниях загрузки для вашей реализации PagingSource , а также для вашей реализации RemoteMediator , если таковая существует.

Для удобства вы можете использовать свойства refresh , append и prepend из CombinedLoadStates для доступа к объекту LoadState для соответствующего типа загрузки. Эти свойства обычно подчиняются состоянию загрузки реализации RemoteMediator , если таковое существует; в противном случае они содержат соответствующее состояние загрузки из реализации PagingSource . Более подробную информацию о базовой логике см. в справочной документации по CombinedLoadStates .

Котлин

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

Ява

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

Ява

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

Однако важно помнить, что только состояния загрузки PagingSource гарантированно синхронизируются с обновлениями пользовательского интерфейса. Поскольку свойства refresh , append и prepend потенциально могут принимать состояние загрузки из PagingSource или RemoteMediator , их синхронность с обновлениями пользовательского интерфейса не гарантируется. Это может вызвать проблемы с пользовательским интерфейсом, когда загрузка завершается до того, как в пользовательский интерфейс будут добавлены какие-либо новые данные.

По этой причине удобные средства доступа хорошо подходят для отображения состояния загрузки в верхнем или нижнем колонтитуле, но в других случаях вам может потребоваться специальный доступ к состоянию загрузки из PagingSource или RemoteMediator . Для этой цели CombinedLoadStates предоставляет свойства source и mediator . Каждое из этих свойств предоставляет объект LoadStates , содержащий объекты LoadState для PagingSource или RemoteMediator соответственно:

Котлин

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

Ява

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

Ява

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

Операторы цепочки на LoadState

Поскольку объект CombinedLoadStates предоставляет доступ ко всем изменениям состояния загрузки, важно фильтровать поток состояния загрузки на основе определенных событий. Это гарантирует, что вы обновите свой пользовательский интерфейс в подходящее время, чтобы избежать зависаний и ненужных обновлений пользовательского интерфейса.

Например, предположим, что вы хотите отобразить пустое представление, но только после завершения начальной загрузки данных. В этом варианте использования необходимо убедиться, что загрузка обновления данных началась, а затем дождаться состояния NotLoading , чтобы подтвердить завершение обновления. Вы должны отфильтровать все сигналы, кроме тех, которые вам нужны:

Котлин

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

Ява

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

Ява

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

В этом примере ожидается обновление состояния загрузки обновления, но он срабатывает только тогда, когда состояние NotLoading . Это гарантирует, что удаленное обновление полностью завершится до того, как произойдет какое-либо обновление пользовательского интерфейса.

API-интерфейсы Stream делают этот тип операций возможным. Ваше приложение может указать необходимые ему события загрузки и обрабатывать новые данные при выполнении соответствующих критериев.

{% дословно %} {% дословно %} {% дословно %} {% дословно %}