确保您的应用在网络连接不可靠或用户离线时仍可使用,从而提供更出色的用户体验。实现此目的的一种方法是同时从网络和本地数据库分页。这样,您的应用可以从本地数据库缓存驱动界面,并且仅当数据库中没有更多数据时才向网络发出请求。
本指南假定您熟悉 Room 持久性库和 Paging 库的基本用法。
协调数据加载
Paging 库为此用例提供了 RemoteMediator
组件。RemoteMediator
在应用缓存数据用尽时作为 Paging 库的信号。您可以使用此信号从网络加载额外数据并将其存储在本地数据库中,然后 PagingSource
可以加载该数据并将其提供给界面以显示。
当需要额外数据时,Paging 库会调用 RemoteMediator
实现中的 load()
方法。这是一个挂起函数,因此可以安全地执行长时间运行的工作。此函数通常从网络源获取新数据并将其保存到本地存储。
此过程适用于新数据,但随着时间的推移,数据库中存储的数据需要失效,例如当用户手动触发刷新时。这由传递给 load()
方法的 LoadType
属性表示。LoadType
通知 RemoteMediator
是否需要刷新现有数据,或获取需要附加或前置到现有列表的额外数据。
通过这种方式,RemoteMediator
可确保您的应用以适当的顺序加载用户希望看到的数据。
分页生命周期

直接从网络分页时,PagingSource
会加载数据并返回一个 LoadResult
对象。PagingSource
实现通过 pagingSourceFactory
参数传递给 Pager
。
当界面需要新数据时,Pager
会调用 PagingSource
中的 load()
方法,并返回一个封装新数据的 PagingData
对象流。每个 PagingData
对象通常在发送到界面显示之前在 ViewModel
中缓存。

RemoteMediator
改变了这种数据流。PagingSource
仍然加载数据;但是当分页数据耗尽时,Paging 库会触发 RemoteMediator
从网络源加载新数据。RemoteMediator
将新数据存储在本地数据库中,因此 ViewModel
中的内存缓存是不必要的。最后,PagingSource
会使自身失效,并且 Pager
会创建一个新实例以从数据库加载最新数据。
基本用法
假设您希望您的应用从以项为键的网络数据源加载 User
项页面到存储在 Room 数据库中的本地缓存。
RemoteMediator
实现有助于将分页数据从网络加载到数据库中,但不会直接将数据加载到界面中。相反,应用将数据库用作单一可信来源。换句话说,应用只显示已缓存到数据库中的数据。PagingSource
实现(例如,Room 生成的实现)负责将缓存数据从数据库加载到界面中。
创建 Room 实体
第一步是使用 Room 持久性库定义一个数据库,该数据库保存来自网络数据源的分页数据的本地缓存。从 使用 Room 将数据保存到本地数据库中描述的 RoomDatabase
实现开始。
接下来,定义一个 Room 实体来表示列表项表,如 使用 Room 实体定义数据中所述。为其提供一个 id
字段作为主键,以及列表项包含的任何其他信息的字段。
Kotlin
@Entity(tableName = "users") data class User(val id: String, val label: String)
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; }
您还必须为这个 Room 实体定义一个数据访问对象 (DAO),如 使用 Room DAO 访问数据中所述。列表项实体的 DAO 必须包含以下方法:
- 一个
insertAll()
方法,用于将项目列表插入到表中。 - 一个将查询字符串作为参数并返回列表结果的
PagingSource
对象的方法。这样,Pager
对象就可以将此表用作分页数据的来源。 - 一个
clearAll()
方法,用于删除表中的所有数据。
Kotlin
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(users: List<User>) @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> @Query("DELETE FROM users") suspend fun 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
时相同。有关选择类型参数的更多信息,请参阅选择键和值类型。
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { // ... } }
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)
。数据存储后,使数据源失效以通知 Paging 库新数据。 - 如果加载成功且收到的项目列表为空,或者它是最后一页索引,则返回
MediatorResult.Success(endOfPaginationReached = true)
。数据存储后,使数据源失效以通知 Paging 库新数据。 - 如果请求导致错误,则返回
MediatorResult.Error
。
- 如果加载成功且收到的项目列表不为空,则将列表项目存储在数据库中并返回
Kotlin
override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // 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. val loadKey = when (loadType) { LoadType.REFRESH -> null // 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. LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true) LoadType.APPEND -> { val 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 MediatorResult.Success( endOfPaginationReached = true ) } lastItem.id } } // Suspending network load via Retrofit. This doesn't need to be // wrapped in a withContext(Dispatcher.IO) { ... } block since // Retrofit's Coroutine CallAdapter dispatches on a worker // thread. val response = networkService.searchUsers( query = query, after = loadKey ) database.withTransaction { 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.users) } MediatorResult.Success( endOfPaginationReached = response.nextKey == null ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } }
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
跳过远程刷新并加载缓存数据。
Kotlin
override suspend fun initialize(): InitializeAction { val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) return if (System.currentTimeMillis() - db.lastUpdated() <= cacheTimeout) { // Cached data is up-to-date, so there is no need to re-fetch // from the network. 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. InitializeAction.LAUNCH_INITIAL_REFRESH } }
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
类似,但您必须以两种方式进行不同的操作:
- 您必须提供返回 DAO 的
PagingSource
对象的查询方法,而不是直接传递PagingSource
构造函数。 - 您必须提供您的
RemoteMediator
实现实例作为remoteMediator
参数。
Kotlin
val userDao = database.userDao() val pager = Pager( config = PagingConfig(pageSize = 50) remoteMediator = 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) () -> 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
实现中的 initialize()
方法返回 LAUNCH_INITIAL_REFRESH
时,数据已过时,必须用新数据替换。任何 PREPEND
或 APPEND
加载请求都将被强制等待远程 REFRESH
加载成功。由于 PREPEND
或 APPEND
请求在 REFRESH
请求之前排队,因此当它们运行时,传递给这些加载调用的 PagingState
可能会过时。
根据数据在本地的存储方式,如果缓存数据的更改导致失效和新数据获取,您的应用可以忽略冗余请求。例如,Room 会在任何数据插入时使查询失效。这意味着当新数据插入到数据库中时,带有刷新数据的新 PagingSource
对象将提供给待处理的加载请求。
解决此数据同步问题对于确保用户看到最相关、最新的数据至关重要。最佳解决方案主要取决于网络数据源分页数据的方式。无论如何,远程键允许您保存有关从服务器请求的最新页面的信息。您的应用可以使用此信息来识别并请求要加载的下一页的正确数据。
管理远程键
远程键是 RemoteMediator
实现用于告诉后端服务接下来要加载哪些数据的键。在最简单的情况下,每个分页数据项都包含一个您可以轻松引用的远程键。但是,如果远程键不对应于单个项,则必须单独存储它们并在 load()
方法中管理它们。
本节介绍如何收集、存储和更新未存储在单个项目中的远程键。
项目键
本节介绍如何使用与单个项目对应的远程键。通常,当 API 以单个项目为键时,项目 ID 作为查询参数传递。参数名称指示服务器应响应所提供 ID 之前或之后的项目。在 User
模型类的示例中,服务器的 id
字段在请求额外数据时用作远程键。
当您的 load()
方法需要管理特定于项目的远程键时,这些键通常是从服务器获取的数据的 ID。刷新操作不需要加载键,因为它们只检索最新数据。类似地,前置操作不需要获取任何额外数据,因为刷新总是从服务器拉取最新数据。
但是,追加操作确实需要 ID。这需要您从数据库中加载最后一项,并使用其 ID 来加载下一页数据。如果数据库中没有项目,则将 endOfPaginationReached
设置为 true,表示需要数据刷新。
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // 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. val loadKey = when (loadType) { LoadType.REFRESH -> null // 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. LoadType.PREPEND -> return MediatorResult.Success( endOfPaginationReached = true ) // Get the last User object id for the next RemoteKey. LoadType.APPEND -> { val 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 MediatorResult.Success( endOfPaginationReached = true ) } lastItem.id } } // Suspending network load via Retrofit. This doesn't need to // be wrapped in a withContext(Dispatcher.IO) { ... } block // since Retrofit's Coroutine CallAdapter dispatches on a // worker thread. val response = networkService.searchUsers(query, loadKey) // Store loaded data, and next key in transaction, so that // they're always consistent. database.withTransaction { 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.users) } // End of pagination has been reached if no users are returned from the // service MediatorResult.Success( endOfPaginationReached = response.users.isEmpty() ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } } }
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); }
页面键
本节介绍如何使用不对应于单个项目的远程键。
添加远程键表
当远程键与列表项没有直接关联时,最好将它们存储在本地数据库中的单独表中。定义一个表示远程键表的 Room 实体:
Kotlin
@Entity(tableName = "remote_keys") data class RemoteKey(val label: String, val nextKey: String?)
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; }
您还必须为 RemoteKey
实体定义一个 DAO:
Kotlin
@Dao interface RemoteKeyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertOrReplace(remoteKey: RemoteKey) @Query("SELECT * FROM remote_keys WHERE label = :query") suspend fun remoteKeyByQuery(query: String): RemoteKey @Query("DELETE FROM remote_keys WHERE label = :query") suspend fun deleteByQuery(query: String) }
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
来确定要加载的下一个键。 - 除了分页数据本身,还插入或存储从网络数据源返回的远程键。
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() val remoteKeyDao = database.remoteKeyDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // 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. val loadKey = when (loadType) { LoadType.REFRESH -> null // 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. LoadType.PREPEND -> return MediatorResult.Success( endOfPaginationReached = true ) // Query remoteKeyDao for the next RemoteKey. LoadType.APPEND -> { val remoteKey = database.withTransaction { remoteKeyDao.remoteKeyByQuery(query) } // 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 (remoteKey.nextKey == null) { return MediatorResult.Success( endOfPaginationReached = true ) } remoteKey.nextKey } } // Suspending network load via Retrofit. This doesn't need to // be wrapped in a withContext(Dispatcher.IO) { ... } block // since Retrofit's Coroutine CallAdapter dispatches on a // worker thread. val response = networkService.searchUsers(query, loadKey) // Store loaded data, and next key in transaction, so that // they're always consistent. database.withTransaction { if (loadType == LoadType.REFRESH) { remoteKeyDao.deleteByQuery(query) userDao.deleteByQuery(query) } // Update RemoteKey for this query. remoteKeyDao.insertOrReplace( RemoteKey(query, response.nextKey) ) // Insert new users into database, which invalidates the // current PagingData, allowing Paging to present the updates // in the DB. userDao.insertAll(response.users) } MediatorResult.Success( endOfPaginationReached = response.nextKey == null ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } } }
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); }
原地刷新
如果您的应用只需要支持从列表顶部进行网络刷新,如前面的示例所示,则您的 RemoteMediator
不需要定义前置加载行为。
但是,如果您的应用需要支持从网络增量加载到本地数据库中,则必须支持从锚点(用户的滚动位置)恢复分页。Room 的 PagingSource
实现为您处理了这一点,但如果您不使用 Room,则可以通过重写 PagingSource.getRefreshKey()
来实现。有关 getRefreshKey()
的示例实现,请参阅定义 PagingSource。
图 4 说明了首先从本地数据库加载数据,然后一旦数据库中没有数据,就从网络加载数据的过程。
其他资源
要了解有关 Paging 库的更多信息,请参阅以下其他资源:
Codelabs
示例
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- 加载和显示分页数据
- 测试您的分页实现
- 迁移到 Paging 3