Концепции и реализация Jetpack Compose
Улучшите пользовательский опыт, обеспечив возможность использования вашего приложения при нестабильном сетевом соединении или в автономном режиме. Один из способов сделать это — одновременно использовать сетевое соединение и локальную базу данных. Таким образом, ваше приложение будет управлять пользовательским интерфейсом из локального кэша базы данных и отправлять запросы в сеть только тогда, когда в базе данных больше нет данных.
В этом руководстве предполагается, что вы знакомы с библиотекой Room для сохранения данных и с базовыми функциями библиотеки Paging .
Загрузка координатных данных
Библиотека Paging предоставляет компонент RemoteMediator для этого варианта использования. RemoteMediator действует как сигнал от библиотеки Paging, когда в приложении заканчиваются кэшированные данные. Вы можете использовать этот сигнал для загрузки дополнительных данных из сети и сохранения их в локальной базе данных, откуда PagingSource может загрузить их и предоставить пользовательскому интерфейсу для отображения.
Когда требуются дополнительные данные, библиотека Paging вызывает метод load() из реализации RemoteMediator . Это функция с приостановкой выполнения, поэтому она безопасна для выполнения длительных операций. Эта функция обычно получает новые данные из сетевого источника и сохраняет их в локальном хранилище.
Этот процесс работает с новыми данными, но со временем данные, хранящиеся в базе данных, требуют обновления, например, когда пользователь вручную запускает обновление. Это представлено свойством LoadType передаваемым методу load() . LoadType сообщает RemoteMediator , нужно ли обновить существующие данные или получить дополнительные данные, которые необходимо добавить или начать с существующего списка.
Таким образом, RemoteMediator гарантирует, что ваше приложение загрузит данные, которые пользователи хотят видеть, в соответствующем порядке.
Жизненный цикл пейджинга

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

RemoteMediator изменяет этот поток данных. PagingSource по-прежнему загружает данные; но когда данные, передаваемые постранично, исчерпаны, библиотека Paging запускает RemoteMediator для загрузки новых данных из сетевого источника. RemoteMediator сохраняет новые данные в локальной базе данных, поэтому кэш в памяти в ViewModel не требуется. Наконец, PagingSource аннулирует себя, и Pager создает новый экземпляр для загрузки свежих данных из базы данных.
Основное использование
Предположим, вы хотите, чтобы ваше приложение загружало страницы User элементов из сетевого источника данных, основанного на элементах, в локальный кэш, хранящийся в базе данных Room.
Реализация 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() должен выполнить следующие шаги:
- Определяйте, какую страницу загрузить из сети в зависимости от типа загрузки и уже загруженных данных.
- Инициировать сетевой запрос.
- Выполняйте действия в зависимости от результата операции погрузки:
- Если загрузка прошла успешно и полученный список элементов не пуст, то сохраните элементы списка в базе данных и верните
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 %}Рекомендуем вам
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Загрузка и отображение постраничных данных
- Протестируйте свою реализацию постраничной навигации.
- Перейти к пейджингу 3