Страница из сети и базы данных (Представления)

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

Улучшите пользовательский опыт, обеспечив возможность использования вашего приложения при нестабильном сетевом соединении или в автономном режиме. Один из способов сделать это — одновременно использовать сетевое соединение и локальную базу данных. Таким образом, ваше приложение будет управлять пользовательским интерфейсом из локального кэша базы данных и отправлять запросы в сеть только тогда, когда в базе данных больше нет данных.

В этом руководстве предполагается, что вы знакомы с библиотекой Room для сохранения данных и с базовыми функциями библиотеки Paging .

Загрузка координатных данных

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

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

Этот процесс работает с новыми данными, но со временем данные, хранящиеся в базе данных, требуют обновления, например, когда пользователь вручную запускает обновление. Это представлено свойством LoadType передаваемым методу load() . LoadType сообщает RemoteMediator , нужно ли обновить существующие данные или получить дополнительные данные, которые необходимо добавить или начать с существующего списка.

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

Жизненный цикл пейджинга

Рисунок 1. Диаграмма жизненного цикла пейджинга с использованием PagingSource и PagingData.

При пейджинге непосредственно из сети PagingSource загружает данные и возвращает объект LoadResult . Реализация PagingSource передается в Pager через параметр pagingSourceFactory .

По мере необходимости ввода новых данных в пользовательский интерфейс, Pager вызывает метод load() из PagingSource и возвращает поток объектов PagingData , инкапсулирующих новые данные. Каждый объект PagingData обычно кэшируется в ViewModel перед отправкой в ​​пользовательский интерфейс для отображения.

Рисунок 2. Схема жизненного цикла пейджинга с использованием PagingSource и RemoteMediator.

RemoteMediator изменяет этот поток данных. PagingSource по-прежнему загружает данные; но когда данные, передаваемые постранично, исчерпаны, библиотека Paging запускает RemoteMediator для загрузки новых данных из сетевого источника. RemoteMediator сохраняет новые данные в локальной базе данных, поэтому кэш в памяти в ViewModel не требуется. Наконец, PagingSource аннулирует себя, и Pager создает новый экземпляр для загрузки свежих данных из базы данных.

Основное использование

Предположим, вы хотите, чтобы ваше приложение загружало страницы User элементов из сетевого источника данных, основанного на элементах, в локальный кэш, хранящийся в базе данных Room.

RemoteMediator загружает данные из сети в базу данных, а PagingSource загружает данные из базы данных. Пейджер использует как RemoteMediator, так и PagingSource для загрузки данных, передаваемых по пейджеру.
Рисунок 3. Схема реализации постраничной навигации с использованием многоуровневого источника данных.

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

Создать объекты комнаты

Первый шаг — использовать библиотеку Room для сохранения данных, чтобы определить базу данных, которая хранит локальный кэш постраничных данных из сетевого источника данных. Начните с реализации RoomDatabase , как описано в разделе «Сохранение данных в локальной базе данных с помощью Room» .

Далее определите сущность Room, представляющую собой таблицу элементов списка, как описано в разделе «Определение данных с помощью сущностей Room» . Укажите поле id в качестве первичного ключа, а также поля для любой другой информации, содержащейся в элементах списка.

Java

@Entity(tableName = "users")
public class User {
  public String id;
  public String label;
}

Java

@Entity(tableName = "users")
public class User {
  public String id;
  public String label;
}

Также необходимо определить объект доступа к данным (DAO) для этой сущности «Комната», как описано в разделе «Доступ к данным с помощью DAO для комнат» . DAO для сущности «Элемент списка» должен включать следующие методы:

  • Метод insertAll() , который вставляет список элементов в таблицу.
  • Метод, который принимает строку запроса в качестве параметра и возвращает объект PagingSource для списка результатов. Таким образом, объект Pager может использовать эту таблицу в качестве источника постраничных данных.
  • Метод clearAll() удаляет все данные из таблицы.

Java

@Dao
interface UserDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertAll(List<User> users);

  @Query("SELECT * FROM users WHERE mLabel LIKE :query")
  PagingSource<Integer, User> pagingSource(String query);

  @Query("DELETE FROM users")
  int clearAll();
}

Java

@Dao
interface UserDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertAll(List<User> users);

  @Query("SELECT * FROM users WHERE mLabel LIKE :query")
  PagingSource<Integer, User> pagingSource(String query);

  @Query("DELETE FROM users")
  int clearAll();
}

Реализуйте удаленный посредник (RemoteMediator).

Основная задача RemoteMediator — загрузка дополнительных данных из сети, когда у Pager заканчиваются данные или существующие данные становятся недействительными. Он включает в себя метод load() , который необходимо переопределить для определения поведения загрузки.

Типичная реализация RemoteMediator включает следующие параметры:

  • query : Строка запроса, определяющая, какие данные следует получить из серверной части.
  • database : база данных Room, которая служит локальным кэшем.
  • networkService : Экземпляр API для серверной части.

Создайте реализацию RemoteMediator<Key, Value> . Типы Key и Value должны совпадать с типами, которые вы бы использовали при определении PagingSource для того же источника сетевых данных. Дополнительную информацию о выборе параметров типа см. в разделе «Выбор типов ключей и значений» .

Java

@UseExperimental(markerClass = ExperimentalPagingApi.class)
class ExampleRemoteMediator extends RxRemoteMediator<Integer, User> {
  private String query;
  private ExampleBackendService networkService;
  private RoomDb database;
  private UserDao userDao;

  ExampleRemoteMediator(
    String query,
    ExampleBackendService networkService, RoomDb database
  ) {
    query = query;
    networkService = networkService;
    database = database;
    userDao = database.userDao();
  }

  @NotNull
  @Override
  public Single<MediatorResult> loadSingle(
    @NotNull LoadType loadType,
    @NotNull PagingState<Integer, User> state
  ) {
    ...
  }
}

Java

class ExampleRemoteMediator extends ListenableFutureRemoteMediator<Integer, User> {
  private String query;
  private ExampleBackendService networkService;
  private RoomDb database;
  private UserDao userDao;
  private Executor bgExecutor;

  ExampleRemoteMediator(
    String query,
    ExampleBackendService networkService,
    RoomDb database,
    Executor bgExecutor
  ) {
    this.query = query;
    this.networkService = networkService;
    this.database = database;
    this.userDao = database.userDao();
    this.bgExecutor = bgExecutor;
  }

  @NotNull
  @Override
  public ListenableFuture<MediatorResult> loadFuture(
    @NotNull LoadType loadType,
    @NotNull PagingState<Integer, User> state
  ) {
    ...
  }
}

Метод load() отвечает за обновление базового набора данных и аннулирование объекта PagingSource . Некоторые библиотеки, поддерживающие постраничную навигацию (например, Room), автоматически обрабатывают аннулирование объектов PagingSource , которые они реализуют.

Метод load() принимает два параметра:

  • PagingState содержит информацию о загруженных страницах, индексе, к которому обращались в последний раз, и объекте PagingConfig , который использовался для инициализации потока постраничной навигации.
  • LoadType указывает тип загрузки: REFRESH , APPEND или PREPEND .

Возвращаемым значением метода load() является объект MediatorResult . MediatorResult может быть либо MediatorResult.Error (который содержит описание ошибки), либо MediatorResult.Success (который содержит сигнал, указывающий, есть ли еще данные для загрузки).

Метод load() должен выполнить следующие шаги:

  1. Определяйте, какую страницу загрузить из сети в зависимости от типа загрузки и уже загруженных данных.
  2. Инициировать сетевой запрос.
  3. Выполняйте действия в зависимости от результата операции погрузки:
    • Если загрузка прошла успешно и полученный список элементов не пуст, то сохраните элементы списка в базе данных и верните MediatorResult.Success(endOfPaginationReached = false) . После сохранения данных аннулируйте источник данных, чтобы уведомить библиотеку пейджинга о новых данных.
    • Если загрузка прошла успешно и полученный список элементов пуст или это последняя страница индекса, верните MediatorResult.Success(endOfPaginationReached = true) . После сохранения данных аннулируйте источник данных, чтобы уведомить библиотеку постраничной навигации о новых данных.
    • Если запрос вызывает ошибку, верните MediatorResult.Error .

Java

@NotNull
@Override
public Single<MediatorResult> loadSingle(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter. For
  // every page after the first, pass the last user ID to let it continue from
  // where it left off. For REFRESH, pass null to load the first page.
  String loadKey = null;
  switch (loadType) {
    case REFRESH:
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Single.just(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Single.just(new MediatorResult.Success(true));
      }

      loadKey = lastItem.getId();
      break;
  }

  return networkService.searchUsers(query, loadKey)
    .subscribeOn(Schedulers.io())
    .map((Function<SearchUserResponse, MediatorResult>) response -> {
      database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
        }

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getNextKey() == null);
    })
    .onErrorResumeNext(e -> {
      if (e instanceof IOException || e instanceof HttpException) {
        return Single.just(new MediatorResult.Error(e));
      }

      return Single.error(e);
    });
}

Java

@NotNull
@Override
public ListenableFuture<MediatorResult> loadFuture(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter. For
  // every page after the first, pass the last user ID to let it continue from
  // where it left off. For REFRESH, pass null to load the first page.
  String loadKey = null;
  switch (loadType) {
    case REFRESH:
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Futures.immediateFuture(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Futures.immediateFuture(new MediatorResult.Success(true));
      }

      loadKey = lastItem.getId();
      break;
  }

  ListenableFuture<MediatorResult> networkResult = Futures.transform(
    networkService.searchUsers(query, loadKey),
    response -> {
      database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
        }

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getNextKey() == null);
    }, bgExecutor);

  ListenableFuture<MediatorResult> ioCatchingNetworkResult =
    Futures.catching(
      networkResult,
      IOException.class,
      MediatorResult.Error::new,
      bgExecutor
    );

  return Futures.catching(
    ioCatchingNetworkResult,
    HttpException.class,
    MediatorResult.Error::new,
    bgExecutor
  );
}

Определите метод инициализации.

В реализациях RemoteMediator также можно переопределить метод initialize() , чтобы проверить, устарели ли кэшированные данные, и решить, следует ли запускать удалённое обновление. Этот метод выполняется до начала загрузки данных, поэтому вы можете внести изменения в базу данных (например, очистить старые данные) до запуска локальной или удалённой загрузки.

Поскольку initialize() — асинхронная функция, вы можете загружать данные, чтобы определить актуальность существующих данных в базе данных. Наиболее распространенный случай — это когда кэшированные данные действительны только в течение определенного периода времени. RemoteMediator может проверить, истек ли этот срок, в этом случае библиотеке Paging необходимо полностью обновить данные. Реализации initialize() должны возвращать InitializeAction следующим образом:

  • В случаях, когда необходимо полностью обновить локальные данные, initialize() должен возвращать InitializeAction.LAUNCH_INITIAL_REFRESH . Это заставляет RemoteMediator выполнить удаленное обновление для полной перезагрузки данных. Любые удаленные загрузки APPEND или PREPEND ожидают успешного завершения загрузки REFRESH , прежде чем продолжить.
  • В случаях, когда локальные данные не нуждаются в обновлении, initialize() должен возвращать InitializeAction.SKIP_INITIAL_REFRESH . Это позволит RemoteMediator пропустить обновление удалённых данных и загрузить кэшированные данные.

Java

@NotNull
@Override
public Single<InitializeAction> initializeSingle() {
  long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
  return mUserDao.lastUpdatedSingle()
    .map(lastUpdatedMillis -> {
      if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) {
        // Cached data is up-to-date, so there is no need to re-fetch
        // from the network.
        return InitializeAction.SKIP_INITIAL_REFRESH;
      } else {
        // Need to refresh cached data from network; returning
        // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's
        // APPEND and PREPEND from running until REFRESH succeeds.
        return InitializeAction.LAUNCH_INITIAL_REFRESH;
      }
    });
}

Java

@NotNull
@Override
public ListenableFuture<InitializeAction> initializeFuture() {
  long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
  return Futures.transform(
    mUserDao.lastUpdated(),
    lastUpdatedMillis -> {
      if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) {
        // Cached data is up-to-date, so there is no need to re-fetch
        // from the network.
        return InitializeAction.SKIP_INITIAL_REFRESH;
      } else {
        // Need to refresh cached data from network; returning
        // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's
        // APPEND and PREPEND from running until REFRESH succeeds.
        return InitializeAction.LAUNCH_INITIAL_REFRESH;
      }
    },
    mBgExecutor);
}

Создать пейджер

Наконец, необходимо создать экземпляр Pager для настройки потока постраничных данных. Это похоже на создание Pager из простого сетевого источника данных, но есть два отличия:

  • Вместо прямой передачи конструктора PagingSource необходимо указать метод запроса, который возвращает объект PagingSource из DAO.
  • В качестве параметра remoteMediator необходимо указать экземпляр вашей реализации RemoteMediator .

Java

UserDao userDao = database.userDao();
Pager<Integer, User> pager = Pager(
  new PagingConfig(/* pageSize = */ 20),
  null, // initialKey,
  new ExampleRemoteMediator(query, database, networkService)
  () -> userDao.pagingSource(query));

Java

UserDao userDao = database.userDao();
Pager<Integer, User> pager = Pager(
  new PagingConfig(/* pageSize = */ 20),
  null, // initialKey
  new ExampleRemoteMediator(query, database, networkService, bgExecutor),
  () -> userDao.pagingSource(query));

Управление удаленными клавишами

Удаленные ключи — это ключи, которые реализация RemoteMediator использует для указания бэкэнд-сервису, какие данные следует загрузить дальше. В простейшем случае каждый элемент постраничных данных содержит удаленный ключ, на который можно легко сослаться. Однако, если удаленные ключи не соответствуют отдельным элементам, то их необходимо хранить отдельно и управлять ими в методе load() .

В этом разделе описывается, как собирать, хранить и обновлять удаленные ключи, которые не хранятся в отдельных элементах.

Ключи от предметов

В этом разделе описывается, как работать с удалёнными ключами, соответствующими отдельным элементам. Как правило, когда API использует ключи, привязанные к отдельным элементам, идентификатор элемента передаётся в качестве параметра запроса. Имя параметра указывает, должен ли сервер отвечать элементами до или после предоставленного идентификатора. В примере с классом модели User поле id с сервера используется в качестве удалённого ключа при запросе дополнительных данных.

Когда методу load() необходимо управлять удаленными ключами, специфичными для элементов, этими ключами обычно являются идентификаторы данных, полученных с сервера. Операциям обновления не нужен ключ загрузки, поскольку они просто получают самые последние данные. Аналогично, операциям добавления данных в начало списка не нужно получать какие-либо дополнительные данные, поскольку обновление всегда получает самые свежие данные с сервера.

Однако для операций добавления требуется идентификатор. Это означает, что необходимо загрузить последний элемент из базы данных и использовать его идентификатор для загрузки следующей страницы данных. Если в базе данных нет элементов, параметр endOfPaginationReached устанавливается в значение true, указывая на необходимость обновления данных.

Java

@NotNull
@Override
public Single>MediatorResult< loadSingle(
  @NotNull LoadType loadType,
  @NotNull PagingState>Integer, User< state
) {
  // The network load method takes an optional String parameter. For every page
  // after the first, pass the String token returned from the previous page to
  // let it continue from where it left off. For REFRESH, pass null to load the
  // first page.
  Single>String< remoteKeySingle = null;
  switch (loadType) {
    case REFRESH:
      // Initial load should use null as the page key, so you can return null
      // directly.
      remoteKeySingle = Single.just(null);
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Single.just(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when
      // appending, since passing null to networkService is only
      // valid for initial load. If lastItem is null it means no
      // items were loaded after the initial REFRESH and there are
      // no more items to load.
      if (lastItem == null) {
        return Single.just(new MediatorResult.Success(true));
      }
      remoteKeySingle = Single.just(lastItem.getId());
      break;
  }

  return remoteKeySingle
    .subscribeOn(Schedulers.io())
    .flatMap((Function<String, Single<MediatorResult>>) remoteKey -> {
      return networkService.searchUsers(query, remoteKey)
        .map(response -> {
          database.runInTransaction(() -> {
            if (loadType == LoadType.REFRESH) {
              userDao.deleteByQuery(query);
            }
            // Insert new users into database, which invalidates the current
            // PagingData, allowing Paging to present the updates in the DB.
            userDao.insertAll(response.getUsers());
          });

          return new MediatorResult.Success(response.getUsers().isEmpty());
        });
    })
    .onErrorResumeNext(e -> {
      if (e instanceof IOException || e instanceof HttpException) {
        return Single.just(new MediatorResult.Error(e));
      }

      return Single.error(e);
    });
}

Java

@NotNull
@Override
public ListenableFuture<MediatorResult> loadFuture(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter.
  // For every page after the first, pass the last user ID to let it continue
  // from where it left off. For REFRESH, pass null to load the first page.
  ResolvableFuture<String> remoteKeyFuture = ResolvableFuture.create();
  switch (loadType) {
    case REFRESH:
      remoteKeyFuture.set(null);
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Futures.immediateFuture(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Futures.immediateFuture(new MediatorResult.Success(true));
      }

      remoteKeyFuture.set(lastItem.getId());
      break;
  }

  return Futures.transformAsync(remoteKeyFuture, remoteKey -> {

    ListenableFuture<MediatorResult> networkResult = Futures.transform(
      networkService.searchUsers(query, remoteKey),
      response -> {
        database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
        }

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getUsers().isEmpty());
    }, bgExecutor);

    ListenableFuture<MediatorResult> ioCatchingNetworkResult =
      Futures.catching(
        networkResult,
        IOException.class,
        MediatorResult.Error::new,
        bgExecutor
      );

    return Futures.catching(
      ioCatchingNetworkResult,
      HttpException.class,
      MediatorResult.Error::new,
      bgExecutor
    );
  }, bgExecutor);
}

Ключи страниц

В этом разделе описывается, как работать с удаленными клавишами, которые не соответствуют отдельным элементам.

Добавить таблицу удаленных ключей

Если удаленные ключи не связаны напрямую с элементами списка, лучше всего хранить их в отдельной таблице в локальной базе данных. Определите сущность «Комната», которая представляет собой таблицу удаленных ключей:

Java

@Entity(tableName = "remote_keys")
public class RemoteKey {
  public String label;
  public String nextKey;
}

Java

@Entity(tableName = "remote_keys")
public class RemoteKey {
  public String label;
  public String nextKey;
}

Также необходимо определить DAO для сущности RemoteKey :

Java

@Dao
interface RemoteKeyDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertOrReplace(RemoteKey remoteKey);

  @Query("SELECT * FROM remote_keys WHERE label = :query")
  Single<RemoteKey> remoteKeyByQuerySingle(String query);

  @Query("DELETE FROM remote_keys WHERE label = :query")
  void deleteByQuery(String query);
}

Java

@Dao
interface RemoteKeyDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertOrReplace(RemoteKey remoteKey);

  @Query("SELECT * FROM remote_keys WHERE label = :query")
  ListenableFuture<RemoteKey> remoteKeyByQueryFuture(String query);

  @Query("DELETE FROM remote_keys WHERE label = :query")
  void deleteByQuery(String query);
}

Загрузка с помощью дистанционных клавиш

Если методу load() необходимо управлять ключами удаленной страницы, его следует определять иначе, чем при стандартном использовании RemoteMediator , используя следующие методы:

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

Java

@NotNull
@Override
public Single<MediatorResult> loadSingle(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional String parameter. For every page
  // after the first, pass the String token returned from the previous page to
  // let it continue from where it left off. For REFRESH, pass null to load the
  // first page.
  Single<RemoteKey> remoteKeySingle = null;
  switch (loadType) {
    case REFRESH:
      // Initial load should use null as the page key, so you can return null
      // directly.
      remoteKeySingle = Single.just(new RemoteKey(mQuery, null));
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Single.just(new MediatorResult.Success(true));
    case APPEND:
      // Query remoteKeyDao for the next RemoteKey.
      remoteKeySingle = mRemoteKeyDao.remoteKeyByQuerySingle(mQuery);
      break;
  }

  return remoteKeySingle
    .subscribeOn(Schedulers.io())
    .flatMap((Function<RemoteKey, Single<MediatorResult>>) remoteKey -> {
      // You must explicitly check if the page key is null when appending,
      // since null is only valid for initial load. If you receive null
      // for APPEND, that means you have reached the end of pagination and
      // there are no more items to load.
      if (loadType != REFRESH && remoteKey.getNextKey() == null) {
        return Single.just(new MediatorResult.Success(true));
      }

      return networkService.searchUsers(query, remoteKey.getNextKey())
        .map(response -> {
          database.runInTransaction(() -> {
            if (loadType == LoadType.REFRESH) {
              userDao.deleteByQuery(query);
              remoteKeyDao.deleteByQuery(query);
            }

            // Update RemoteKey for this query.
            remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey()));

            // Insert new users into database, which invalidates the current
            // PagingData, allowing Paging to present the updates in the DB.
            userDao.insertAll(response.getUsers());
          });

          return new MediatorResult.Success(response.getNextKey() == null);
        });
    })
    .onErrorResumeNext(e -> {
      if (e instanceof IOException || e instanceof HttpException) {
        return Single.just(new MediatorResult.Error(e));
      }

      return Single.error(e);
    });
}

Java

@NotNull
@Override
public ListenableFuture<MediatorResult> loadFuture(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter. For
  // every page after the first, pass the last user ID to let it continue from
  // where it left off. For REFRESH, pass null to load the first page.
  ResolvableFuture<RemoteKey> remoteKeyFuture = ResolvableFuture.create();
  switch (loadType) {
    case REFRESH:
      remoteKeyFuture.set(new RemoteKey(query, null));
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Futures.immediateFuture(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Futures.immediateFuture(new MediatorResult.Success(true));
      }

      // Query remoteKeyDao for the next RemoteKey.
      remoteKeyFuture.setFuture(
        remoteKeyDao.remoteKeyByQueryFuture(query));
      break;
  }

  return Futures.transformAsync(remoteKeyFuture, remoteKey -> {
    // You must explicitly check if the page key is null when appending,
    // since null is only valid for initial load. If you receive null
    // for APPEND, that means you have reached the end of pagination and
    // there are no more items to load.
    if (loadType != LoadType.REFRESH && remoteKey.getNextKey() == null) {
      return Futures.immediateFuture(new MediatorResult.Success(true));
    }

    ListenableFuture<MediatorResult> networkResult = Futures.transform(
      networkService.searchUsers(query, remoteKey.getNextKey()),
      response -> {
        database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
          remoteKeyDao.deleteByQuery(query);
        }

        // Update RemoteKey for this query.
        remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey()));

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getNextKey() == null);
    }, bgExecutor);

    ListenableFuture<MediatorResult> ioCatchingNetworkResult =
      Futures.catching(
        networkResult,
        IOException.class,
        MediatorResult.Error::new,
        bgExecutor
      );

    return Futures.catching(
      ioCatchingNetworkResult,
      HttpException.class,
      MediatorResult.Error::new,
      bgExecutor
    );
  }, bgExecutor);
}

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

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

Кодлабс

Образцы

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