Загрузка и отображение постраничных данных

Библиотека подкачки предоставляет мощные возможности для загрузки и отображения постраничных данных из более крупного набора данных. В этом руководстве показано, как использовать библиотеку подкачки для настройки потока выгружаемых данных из сетевого источника данных и отображения его в RecyclerView .

Определить источник данных

Первым шагом является определение реализации PagingSource для идентификации источника данных. Класс API PagingSource включает метод load() , который вы переопределяете, чтобы указать, как получить постраничные данные из соответствующего источника данных.

Используйте класс PagingSource напрямую, чтобы использовать сопрограммы Kotlin для асинхронной загрузки. Библиотека подкачки также предоставляет классы для поддержки других асинхронных фреймворков:

  • Чтобы использовать RxJava, вместо этого реализуйте RxPagingSource .
  • Чтобы использовать ListenableFuture из Guava, вместо этого реализуйте ListenableFuturePagingSource .

Выберите типы ключей и значений

PagingSource<Key, Value> имеет два параметра типа: Key и Value . Ключ определяет идентификатор, используемый для загрузки данных, а значение — это тип самих данных. Например, если вы загружаете страницы объектов User из сети, передавая номера страниц Int в Retrofit , выберите Int в качестве типа Key и User в качестве типа Value .

Определите источник подкачки

В следующем примере реализуется PagingSource , который загружает страницы элементов по номеру страницы. Тип KeyInt , а тип ValueUser .

Котлин

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {
    try {
      // Start refresh at page 1 if undefined.
      val nextPageNumber = params.key ?: 1
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = response.nextPageNumber
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

Ява

class ExamplePagingSource extends RxPagingSource<Integer, User> {
  @NonNull
  private ExampleBackendService mBackend;
  @NonNull
  private String mQuery;

  ExamplePagingSource(@NonNull ExampleBackendService backend,
    @NonNull String query) {
    mBackend = backend;
    mQuery = query;
  }

  @NotNull
  @Override
  public Single<LoadResult<Integer, User>> loadSingle(
    @NotNull LoadParams<Integer> params) {
    // Start refresh at page 1 if undefined.
    Integer nextPageNumber = params.getKey();
    if (nextPageNumber == null) {
      nextPageNumber = 1;
    }

    return mBackend.searchUsers(mQuery, nextPageNumber)
      .subscribeOn(Schedulers.io())
      .map(this::toLoadResult)
      .onErrorReturn(LoadResult.Error::new);
  }

  private LoadResult<Integer, User> toLoadResult(
    @NonNull SearchUserResponse response) {
    return new LoadResult.Page<>(
      response.getUsers(),
      null, // Only paging forward.
      response.getNextPageNumber(),
      LoadResult.Page.COUNT_UNDEFINED,
      LoadResult.Page.COUNT_UNDEFINED);
  }

  @Nullable
  @Override
  public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    Integer anchorPosition = state.getAnchorPosition();
    if (anchorPosition == null) {
      return null;
    }

    LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
    if (anchorPage == null) {
      return null;
    }

    Integer prevKey = anchorPage.getPrevKey();
    if (prevKey != null) {
      return prevKey + 1;
    }

    Integer nextKey = anchorPage.getNextKey();
    if (nextKey != null) {
      return nextKey - 1;
    }

    return null;
  }
}

Ява

class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> {
  @NonNull
  private ExampleBackendService mBackend;
  @NonNull
  private String mQuery;
  @NonNull
  private Executor mBgExecutor;

  ExamplePagingSource(
    @NonNull ExampleBackendService backend,
    @NonNull String query, @NonNull Executor bgExecutor) {
    mBackend = backend;
    mQuery = query;
    mBgExecutor = bgExecutor;
  }

  @NotNull
  @Override
  public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) {
    // Start refresh at page 1 if undefined.
    Integer nextPageNumber = params.getKey();
    if (nextPageNumber == null) {
      nextPageNumber = 1;
    }

    ListenableFuture<LoadResult<Integer, User>> pageFuture =
      Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber),
      this::toLoadResult, mBgExecutor);

    ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture =
      Futures.catching(pageFuture, HttpException.class,
      LoadResult.Error::new, mBgExecutor);

    return Futures.catching(partialLoadResultFuture,
      IOException.class, LoadResult.Error::new, mBgExecutor);
  }

  private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) {
    return new LoadResult.Page<>(response.getUsers(),
    null, // Only paging forward.
    response.getNextPageNumber(),
    LoadResult.Page.COUNT_UNDEFINED,
    LoadResult.Page.COUNT_UNDEFINED);
  }

  @Nullable
  @Override
  public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    Integer anchorPosition = state.getAnchorPosition();
    if (anchorPosition == null) {
      return null;
    }

    LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
    if (anchorPage == null) {
      return null;
    }

    Integer prevKey = anchorPage.getPrevKey();
    if (prevKey != null) {
      return prevKey + 1;
    }

    Integer nextKey = anchorPage.getNextKey();
    if (nextKey != null) {
      return nextKey - 1;
    }

    return null;
  }
}

Типичная реализация PagingSource передает параметры, предоставленные в его конструкторе, методу load() для загрузки соответствующих данных для запроса. В приведенном выше примере эти параметры следующие:

  • backend : экземпляр серверной службы, предоставляющий данные.
  • query : поисковый запрос для отправки в службу, указанную backend

Объект LoadParams содержит информацию о выполняемой операции загрузки. Сюда входит ключ для загрузки и количество загружаемых элементов.

Объект LoadResult содержит результат операции загрузки. LoadResult — это запечатанный класс, который принимает одну из двух форм, в зависимости от того, удался ли вызов load() :

  • Если загрузка прошла успешно, верните объект LoadResult.Page .
  • Если загрузка не удалась, верните объект LoadResult.Error .

На следующем рисунке показано, как функция load() в этом примере получает ключ для каждой загрузки и предоставляет ключ для последующей загрузки.

При каждом вызове load() SamplePagingSource принимает текущий ключ.     и возвращает следующий ключ для загрузки.
Рис. 1. Диаграмма, показывающая, как load() использует и обновляет ключ.

Реализация PagingSource также должна реализовать метод getRefreshKey() , который принимает объект PagingState в качестве параметра. Он возвращает ключ для передачи в метод load() , когда данные обновляются или становятся недействительными после первоначальной загрузки. Библиотека подкачки автоматически вызывает этот метод при последующих обновлениях данных.

Обработка ошибок

Запросы на загрузку данных могут завершиться неудачно по ряду причин, особенно при загрузке по сети. Сообщайте об ошибках, обнаруженных во время загрузки, возвращая объект LoadResult.Error из метода load() .

Например, вы можете выявлять и сообщать об ошибках загрузки в ExamplePagingSource из предыдущего примера, добавив в метод load() следующее:

Котлин

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

Ява

return backend.searchUsers(searchTerm, nextPageNumber)
  .subscribeOn(Schedulers.io())
  .map(this::toLoadResult)
  .onErrorReturn(LoadResult.Error::new);

Ява

ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(
  backend.searchUsers(query, nextPageNumber), this::toLoadResult,
  bgExecutor);

ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(
  pageFuture, HttpException.class, LoadResult.Error::new,
  bgExecutor);

return Futures.catching(partialLoadResultFuture,
  IOException.class, LoadResult.Error::new, bgExecutor);

Дополнительные сведения об обработке ошибок Retrofit см. в примерах в справочнике по API PagingSource .

PagingSource собирает и доставляет объекты LoadResult.Error в пользовательский интерфейс, чтобы вы могли воздействовать на них. Дополнительные сведения о представлении состояния загрузки в пользовательском интерфейсе см. в разделе Управление и представление состояний загрузки .

Настройте поток PagingData

Далее вам понадобится поток выгружаемых данных из реализации PagingSource . Настройте поток данных в вашей ViewModel . Класс Pager предоставляет методы, которые предоставляют реактивный поток объектов PagingData из PagingSource . Библиотека подкачки поддерживает использование нескольких типов потоков, включая Flow , LiveData , а также типы Flowable и Observable из RxJava.

Когда вы создаете экземпляр Pager для настройки реактивного потока, вы должны предоставить экземпляру объект конфигурации PagingConfig и функцию, которая сообщает Pager как получить экземпляр вашей реализации PagingSource :

Котлин

val flow = Pager(
  // Configure how data is loaded by passing additional properties to
  // PagingConfig, such as prefetchDistance.
  PagingConfig(pageSize = 20)
) {
  ExamplePagingSource(backend, query)
}.flow
  .cachedIn(viewModelScope)

Ява

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
  new PagingConfig(/* pageSize = */ 20),
  () -> ExamplePagingSource(backend, query));

Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager);
PagingRx.cachedIn(flowable, viewModelScope);

Ява

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
  new PagingConfig(/* pageSize = */ 20),
  () -> ExamplePagingSource(backend, query));

PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);

Оператор cachedIn() делает поток данных доступным для совместного использования и кэширует загруженные данные с помощью предоставленного CoroutineScope . В этом примере используется viewModelScope предоставленный артефактом жизненного цикла lifecycle-viewmodel-ktx .

Объект Pager вызывает метод load() из объекта PagingSource , предоставляя ему объект LoadParams и получая взамен объект LoadResult .

Определить адаптер RecyclerView

Вам также необходимо настроить адаптер для получения данных в список RecyclerView . Для этой цели библиотека Paging предоставляет класс PagingDataAdapter .

Определите класс, расширяющий PagingDataAdapter . В этом примере UserAdapter расширяет PagingDataAdapter , предоставляя адаптер RecyclerView для элементов списка типа User и используя UserViewHolder в качестве держателя представления :

Котлин

class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) :
  PagingDataAdapter<User, UserViewHolder>(diffCallback) {
  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ): UserViewHolder {
    return UserViewHolder(parent)
  }

  override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
    val item = getItem(position)
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item)
  }
}

Ява

class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
  UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
    super(diffCallback);
  }

  @NonNull
  @Override
  public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new UserViewHolder(parent);
  }

  @Override
  public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
    User item = getItem(position);
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

Ява

class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
  UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
    super(diffCallback);
  }

  @NonNull
  @Override
  public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new UserViewHolder(parent);
  }

  @Override
  public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
    User item = getItem(position);
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

Ваш адаптер также должен определить методы onCreateViewHolder() и onBindViewHolder() и указать DiffUtil.ItemCallback . Это работает так же, как обычно при определении адаптеров списка RecyclerView :

Котлин

object UserComparator : DiffUtil.ItemCallback<User>() {
  override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
    // Id is unique.
    return oldItem.id == newItem.id
  }

  override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
    return oldItem == newItem
  }
}

Ява

class UserComparator extends DiffUtil.ItemCallback<User> {
  @Override
  public boolean areItemsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    // Id is unique.
    return oldItem.id.equals(newItem.id);
  }

  @Override
  public boolean areContentsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    return oldItem.equals(newItem);
  }
}

Ява

class UserComparator extends DiffUtil.ItemCallback<User> {
  @Override
  public boolean areItemsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    // Id is unique.
    return oldItem.id.equals(newItem.id);
  }

  @Override
  public boolean areContentsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    return oldItem.equals(newItem);
  }
}

Отображение выгруженных данных в вашем пользовательском интерфейсе

Теперь, когда вы определили PagingSource , создали способ для вашего приложения генерировать поток PagingData и определили PagingDataAdapter , вы готовы соединить эти элементы вместе и отображать постраничные данные в своей активности.

Выполните следующие шаги в методе onCreate вашей активности или фрагменте onViewCreated :

  1. Создайте экземпляр класса PagingDataAdapter .
  2. Передайте экземпляр PagingDataAdapter в список RecyclerView , в котором вы хотите отобразить выгружаемые данные.
  3. Наблюдайте за потоком PagingData и передайте каждое сгенерированное значение в метод submitData() вашего адаптера.

Котлин

val viewModel by viewModels<ExampleViewModel>()

val pagingAdapter = UserAdapter(UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter

// Activities can use lifecycleScope directly; fragments use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  viewModel.flow.collectLatest { pagingData ->
    pagingAdapter.submitData(pagingData)
  }
}

Ява

ExampleViewModel viewModel = new ViewModelProvider(this)
  .get(ExampleViewModel.class);

UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
  R.id.recycler_view);
recyclerView.adapter = pagingAdapter

viewModel.flowable
  // Using AutoDispose to handle subscription lifecycle.
  // See: https://github.com/uber/AutoDispose.
  .to(autoDisposable(AndroidLifecycleScopeProvider.from(this)))
  .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));

Ява

ExampleViewModel viewModel = new ViewModelProvider(this)
  .get(ExampleViewModel.class);

UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
  R.id.recycler_view);
recyclerView.adapter = pagingAdapter

// Activities can use getLifecycle() directly; fragments use
// getViewLifecycleOwner().getLifecycle().
viewModel.liveData.observe(this, pagingData ->
  pagingAdapter.submitData(getLifecycle(), pagingData));

Список RecyclerView теперь отображает постраничные данные из источника данных и при необходимости автоматически загружает другую страницу.

Дополнительные ресурсы

Чтобы узнать больше о библиотеке подкачки, см. следующие дополнительные ресурсы:

Кодлабы

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