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

Концепции и реализация Jetpack Compose

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

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

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

Библиотека Paging предоставляет доступ к состоянию загрузки для использования в пользовательском интерфейсе через объект 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
  }
}

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

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

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

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

Сначала создайте класс-хранилище представления, который будет хранить ссылки на представления загрузки и ошибок на вашем экране. Создайте функцию bind() , которая принимает в качестве параметра ` LoadState . Эта функция должна переключать видимость представления в зависимости от параметра `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
  }
}

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

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

Котлин

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

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

Котлин

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

Вместо этого вы можете вызвать 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
  }
}

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

Однако важно помнить, что только состояния загрузки 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
  }
}

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

Операторы цепочки в 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) }
}

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

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

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

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}