本篇是Paging3源码分析的下篇,将重点介绍RemoteMediator
的实现原理。网络上有很多的文章介绍这个多级数据源工具类,但是多多少少有点问题,一般都没有彻底理解清楚RemoteMediator
整个过请求的流程。本文将从源码角度解析RemoteMediator
的实现原理,同时也会分享RemoteMediator
的一些小建议。
本文内容续接上篇内容,建议先看一下上篇文章:Jetpack 源码分析(五) - Paging3源码分析(上)。
本文主要内容如下:
- 多级数据源的请求过程。
- 分别分析
RemoteMediator
和PagingSource
的实现细节。- Refresh操作,Prepend操作,Append操作在多级数据源和单一数据源中的不同。
- 关于
RemoteMediator
使用的一些小建议。
本文参考资料:
注意,本文Paging源码均来自于3.0.0-alpha08版本。
1. RemoteMediator的请求过程
相比于单一数据源,RemoteMediator
多了一个过程--从网络上获取数据放到数据库中。那么在多级数据源中,怎么将数据库中的数据拿到UI层去显示呢?这个就要说到PagingSource
。
在这之前,我先对PaingSource
和RemoteMediator
做一个解释,方便大家理解,因为它俩的工作是不一样的。
PaingSource
:用于获取UI层需要的数据,且只能从一个地方获取,这也就是所谓的单一数据源
。UI层需要的数据都是通过该类来获取的,包括在多级数据源里面,PaingSource
负责从数据库里面获取数据。RemoteMediator
:主要的作用也是获取数据,只是它是从网络上(或者其他地方)获取数据,然后放到本地数据库,供PaingSource
从本地数据库中获取数据。需要特别注意的是,RemoteMediator
的数据不会直接用于UI显示,而是保存在数据库中。
同时,我画了一张图来帮助大家来理解这两个类是如何配合工作的(官网的流程图很容易误解,以为RemoteMediator
获取的数据也会用于Ui层显示)。
整个请求过程如下:
首先,当是第一次请求时,
RemoteMediator
会进行一次刷新操作,此时会请求到第一批数据,同时会将这这批数据放到本地数据库里面,此时对应的PagingSource对应的从数据库加载数据(需要注意的是,RemoteMediator
和PagingSource
是搭配使用的)。PagingSource的请求过程跟普通的请求类似,这个我们已经在上篇文章介绍过了,有兴趣的同学可以看看:Jetpack 源码分析(五) - Paging3源码分析(上)。最终会通过发送PageEvent将PagingSource获取的数据传递给Ui层。
不过在这个过程中,我们有两个问题:
- 当PagingSource发现数据不够了,怎么通知
RemoteMediator
继续请求。- 除了第一个页面数据的加载,
RemoteMediator
是怎么加载其他页面的数据,以及加载完成之后怎么通知PagingSource获取的呢?
对于这两个问题,这里就不展开分析。下面我们将重点分析这两个问题和其他很多的问题,比如,之前介绍PagPresenter
的时候,说它内部存储了所有的数据,在RemoteMediator
中就不成立;以及,即使不手动刷新,PagingSource也会进行Refresh操作。
那么多级数据源是怎么从数据库里面获取的数据呢?我们在ViewModel内部定义Flow时,直接从Dao里面获取一个PagingSource 对象,并不是我们自己定义的。这个PagingSource
实际上是一个LegacyPagingSource
,是Paging3框架内部的一个 实现。从数据库获取数据的整个过程,主要是通过LegacyPagingSource
的load方法,调到了LimitOffsetDataSource
里面去了(注意这里是DataSource,对的,就是Paging2里面的DataSource),LimitOffsetDataSource
是PositionalDataSource
的子类,内部处理了从数据库获取数据的操作。也就是说,LegacyPagingSource
其实只是一个Wrapper,用来抹平Paging2和Paging3之间的差异。
2. RemoteMediator的触发
我们知道RemoteMediator
请求是通过load方法进行,那么哪里在调用这个方法呢?在PageFetcher
和PageFetcherSnapshot
内部并没有直接调用调用RemoteMediator
的方法,而是通过RemoteMediatorAccessor
来辅助调用的。RemoteMediatorAccessor
内部封装了很多RemoteMediator
的调用逻辑,包括首次加载和加载更多,主要是通过内部的launchRefresh
和launchBoundary
完成对RemoteMediator
的调用。同时关于RemoteMediatorAccessor
,我们还需要注意一点,该对象只会在首次刷新创建一次,这一点跟PageFetcherSnapshot
有很大的不同,之所以要这样做,是因为RemoteMediatorAccessor
有很多全局的状态,不能因为Refresh而丢失了。
RemoteMediator
的触发请求主要分为两种:Refresh和Append(Prepend)。我们分开来看一下他们的细节。
(1). Refresh
在PageFetcherSnapshot
内部有一个Flow对象--pageEventFlow
,这个对象初始化的时候,定义几段代码,用来实现RemoteMediator
触发Refresh请求,主要代码如下:
if (triggerRemoteRefresh) {
remoteMediatorConnection?.let {
val pagingState = stateLock.withLock { state.currentPagingState(null) }
it.requestLoad(REFRESH, pagingState)
}
}
这段代码非常的简单,但是内部蕴含的信息可不少,主要有三点:
- 首先,判断是否
triggerRemoteRefresh
是否为true,为true进行Refresh操作。为啥要判断这个变量呢?因为这段代码会调用的话,表示在进行Refresh操作,但是不代表RemoteMediator
必须要刷新数据(RemoteMediator
刷新数据时,需要将数据库中旧数据清除掉。)。在单一数据源中,只要不手动Refresh,可能永远不会有第二次Refrsh操作进行(这里只是说的可能,因为不能保证100%,PagingConfig
里面的jumpThreshold
字段会打破这个规则),但是在RemoteMediator
中,如果本地数据库中的数据不够了,PagingSource可能会触发多次Refresh(正常滑动触发的),所以上述的代码可能会调用多次。因此需要通过triggerRemoteRefresh
来过滤条件。同时从另一个方面来看,其他地方可以手动的调用PagingSource的invalidate
和Adapter的refresh
方法来触发刷新,那么这两个方法有啥区别:
(1).invalidate
只是表示当前PagingSource失效了,会重新创建的创建一个新的PagingSource,这个过程不会影响原来的已有的数据。这个方法一般不允许外部手动调用。
(2).refresh
表示需要所有的数据清空,重新进行请求。比如说,我们进行了下拉刷新,此时就会调用这个方法。
同时,我们从两个方法实现也能看出来区别,refresh
给refreshChannel
传的是true,即triggerRemoteRefresh
为true;invalidate
方法传的是false,即triggerRemoteRefresh
为false。为了理解清晰,介绍简单,我将refresh
触发的刷新称之为完全刷新,invalidate
触发的刷新称之为不完全刷新,下述内容统一用这个来表示。- 将
PageFetcherSnapshotState
内部的PagingState
设置为null,这一步主要是为了辅助完全刷新。在Paging刷新过程中,会获取Refresh key,用来判断加载哪部分的数据;如果这个key为空,表示是完全刷新,如果是不为空,那么表示是不完全刷新,这部分的代码在LegacyPagingSource
的getRefreshKey
方法里面,有兴趣的同学可以看看。- 调用
RemoteMediatorConnection
的requestLoad
方法,进行刷新的数据请求。requestLoad
方法非常的重要,因为RemoteMediator
在触发网络请求时,都是通过这个方法实现的。
接下来,我们来分析一下requestLoad
方法,直接来看代码:
override fun requestLoad(loadType: LoadType, pagingState: PagingState<Key, Value>) {
// 1. 往任务队列中添加一个任务。
val newRequest = accessorState.use {
it.add(loadType, pagingState)
}
// 进行网络请求。
if (newRequest) {
when (loadType) {
LoadType.REFRESH -> launchRefresh()
else -> launchBoundary()
}
}
}
requestLoad
方法内部主要是做了两件事:
- 通过add方法往任务队列里面添加一个任务。在
RemoteMediatorAccessImpl
内部,维护了一个pendingRequests
队列,里面存储着三种LoadType的任务。在添加的时候主要是check两件事:首先判断当前任务队列中是否已经有对应LoadType的任务;其次,当前任务是否处于未锁定的状态。只有这两个条件同时满足,才能添加成功,也才能进行第二步操作。- 调用
launchRefresh
方法进行网络请求。
我们来看一下launchRefresh
:
private fun launchRefresh() {
scope.launch {
var launchAppendPrepend = false
isolationRunner.runInIsolation(
priority = PRIORITY_REFRESH
) {
val pendingPagingState = accessorState.use {
it.getPendingRefresh()
}
pendingPagingState?.let {
// 调用RemoteMediator的load方法,进行网络请求。
val loadResult = remoteMediator.load(LoadType.REFRESH, pendingPagingState)
launchAppendPrepend = when (loadResult) {
is MediatorResult.Success -> {
// 更新状态,并且从队列中移除相关任务。
accessorState.use {
it.clearPendingRequests()
it.setBlockState(LoadType.APPEND, UNBLOCKED)
it.setBlockState(LoadType.PREPEND, UNBLOCKED)
it.setError(LoadType.APPEND, null)
it.setError(LoadType.PREPEND, null)
}
false
}
is MediatorResult.Error -> {
// 如果请求失败,那么看看队列中是否Append或者Prepend的任务,如果有的话,那么就
// 执行。
accessorState.use {
it.clearPendingRequest(LoadType.REFRESH)
it.setError(LoadType.REFRESH, LoadState.Error(loadResult.throwable))
it.getPendingBoundary() != null
}
}
}
}
}
if (launchAppendPrepend) {
launchBoundary()
}
}
}
在launchRefresh
方法里面主要是做了两件事:
- 调用
RemoteMediator
的load方法。因为load方法是自己定义,所以做了啥事,我们都很清楚,这里就不展开了。- 其次,就是更新对应的状态。如果请求成功的话,那么会把
APPEND
和PREPEND
释放,保证后面可以正常进行操作;如果是请求失败的话,除了更新状态之外,还通过调用getPendingBoundary
判断当前任务队列是否有Append
和Prepend
的任务,如果有的话,就会调用进行请求,即调用launchBoundary
。
(2). Append
为了简单起见,这里只看Append的场景,Prepend跟Append比较类似,这里就不赘述了。
Refresh的触发过程,我们已经理解了,接下来我们来看一下Append的触发过程。Append是怎么触发的呢?用一句话来总结,就是当PagingSource加载完成数据后,根据请求回来的数据(包括PagingSource的Refresh和Append两种方式)来判断是否需要触发RemoteMediator
的Append操作。比如下述代码:
if (remoteMediatorConnection != null) {
if (result.prevKey == null || result.nextKey == null) {
val pagingState =
stateLock.withLock { state.currentPagingState(lastHint) }
if (result.prevKey == null) {
remoteMediatorConnection.requestLoad(PREPEND, pagingState)
}
if (result.nextKey == null) {
remoteMediatorConnection.requestLoad(APPEND, pagingState)
}
}
}
PagingSource的Refresh和Append关于是否进行RemoteMediator
的Append操作的判断条件非常相似,就是看请求回来的数据的nextKey(prevKey)是否为空,如果为空就表示需要进行RemoteMediator
的Append操作,即需要从网络拉取新的数据。那么nextKey
为空表示的是什么意思呢?
总的来说,
nextKey
为空就表示当前数据已经加载到数据库种的已有数据的边界,此时就必须要从网络网络上加载下一页数据了,否则的话,用户马上就要滑不动了。那么为啥nextKey
就表示已经到了数据边界呢?在Paging2里面,数据请求有一个概念,就是totalCount
,如果最后一个数据项的位置等于这个totalCount
,那么表示已经到了边界,此时nextKey
就会为空。
这里的nextKey
涉及到了PagingSource的加载过程,以及LegacyPagingSource
和LimitOffsetDataSource
的加载,我们先不展开分析,后续有内容会重点分析这个。
Append的触发最终也调用到了RemoteMediatorConnection
的requestLoad
方法里面,我们之前已经看过这个方法了,这里直接看RemoteMediatorAccessor
的launchBoundary
方法:
private fun launchBoundary() {
scope.launch {
isolationRunner.runInIsolation(
priority = PRIORITY_APPEND_PREPEND
) {
while (true) {
val (loadType, pendingPagingState) = accessorState.use {
it.getPendingBoundary()
} ?: break
when (val loadResult = remoteMediator.load(loadType, pendingPagingState)) {
is MediatorResult.Success -> {
accessorState.use {
it.clearPendingRequest(loadType)
if (loadResult.endOfPaginationReached) {
it.setBlockState(loadType, COMPLETED)
}
}
}
is MediatorResult.Error -> {
accessorState.use {
it.clearPendingRequest(loadType)
it.setError(loadType, LoadState.Error(loadResult.throwable))
}
}
}
}
}
}
}
launchBoundary
做的事非常的简单,就是调用RemoteMediator
的load
方法,请求下一批数据到数据库中。操作跟Refresh基本类似,这里就不再分析了。
到此,关于RemoteMediator
触发过程的内容就结束了,在这里,我们对此做一个小小的总结,以便大家脑海中有一个印象。
Paging3内部的刷新可以分为两种:完全刷新(调用
PageFetcher
的refresh
方法)和不完全刷新(调用PageFetcher
的invalidate
方法)。这两种刷新不同点在于,完全刷新时,RemoteMediator
会清空已有的数据,重新请求数据;而不完全刷新则不会。这个主要通过PageFetcherSnapshot
的triggerRemoteRefresh
来控制的。RemoteMediator
的刷新主要是通过RemoteMediatorConnection
的requestLoad方法触发,其方法内部调用launchRefresh
方法,进而调用了RemoteMediator
的load方法。需要特别注意的是,requestLoad
方法是外部(PageFetcherSnapshot)触发RemoteMediator
加载网络数据唯一途径。
RemoteMediator
加载更多的触发是在PagingSource请求完成之后才进行的,当发现已经到了数据边界,此时通过requestLoad
方法加载下一页的数据。RemoteMediatorConnection
内部通过launchBoundary
方法触发RemoteMediator
的load方法。关于数据边界,主要是通过nextKey是为空来判断的,这个涉及到PagingSource的加载过程,我们马上会分析。
3. PagingSource和DataSource的加载
前面已经说过了,在多级数据源中,PagingSource是LegacyPagingSource
,DataSource是LimitOffsetDataSource
。其中LegacyPagingSource
只起到了一个桥梁作用,保证在Paging3里面能使用Paging2的DataSource。
我们直接来看LegacyPagingSource
的load
方法:
override suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value> {
val type = when (params) {
is LoadParams.Refresh -> REFRESH
is LoadParams.Append -> APPEND
is LoadParams.Prepend -> PREPEND
}
val dataSourceParams = Params(
type,
params.key,
params.loadSize,
params.placeholdersEnabled,
@Suppress("DEPRECATION")
params.pageSize
)
return withContext(fetchDispatcher) {
dataSource.load(dataSourceParams).run {
LoadResult.Page(
data,
@Suppress("UNCHECKED_CAST")
if (data.isEmpty() && params is LoadParams.Prepend) null else prevKey as Key?,
@Suppress("UNCHECKED_CAST")
if (data.isEmpty() && params is LoadParams.Append) null else nextKey as Key?,
itemsBefore,
itemsAfter
)
}
}
}
load方法的实现很简单,最终调用了LimitOffsetDataSource
的load方法。不过这里有一点我们需要注意:
当请求返回的数据为空,key会为空。数据为空,表示本地数据库没有更多的数据可以加载,也就是说已经加载到边界了,所以需要告诉
RemoteMediator
从网络上请求更多的数据。
关于这种情况,有一个问题:当执行PagingSource发现没有更多的数据,此时需要从网络上获取数据,那么数据请求回来之后,怎么通知PagingSource来重新加载数据呢?这个就得说说Room的实现,Room在初始化的时候,给我们的表创建了一个触发器,用以监听表的更新,插入和删除三种操作。当有新的数据更新到数据库中去的时候,触发器会发送一个invalidate
的通知,这个通知会调用PagingSource的invalidate
方法,从而导致PagingSource重新创建和重新加载,此时就是所谓在上下滑动过程也会触发PagingSource的Refresh操作。关于触发器的逻辑,有兴趣的同学可以看看Room的一个类:InvalidationTracker
。但是按照正常逻辑来说,PagingSource重建之后,Refresh获取数据应该是全新的,怎么能保证数据前后衔接上呢?这是因为initialKey
的存在,因为在scan方法的时候会拿到创建之前的key,如下:
val flow: Flow<PagingData<Value>> = channelFlow {
// ......
refreshChannel.asFlow()
.onStart {
// ......
}
.scan(null) {
// ......
@OptIn(ExperimentalPagingApi::class)
val initialKey: Key? = previousGeneration?.refreshKeyInfo()
?.let { pagingSource.getRefreshKey(it) }
?: initialKey
// ......
}
// ......
}
这里获取initialKey
主要是通过PagingSource的getRefreshKey
方法。这个方法在之前说不完全刷新时就提到了,感兴趣的同学可以看看(内部主要通过PagingState
的anchorPosition
来计算key,完全刷新时,anchorPosition
要么为0,要么为空,所以不会衔接之前的数据)。
这里给大家补充了一下额外的知识,我们继续看一下DataSource的loadInitial
方法(load方法调用了loadInitial
方法,只是在load方法里面有一些计算,这些计算逻辑有兴趣的同学可以自行看看,这里就不讲解了):
internal suspend fun loadInitial(params: LoadInitialParams) =
suspendCancellableCoroutine<BaseResult<T>> { cont ->
loadInitial(
params,
object : LoadInitialCallback<T>() {
override fun onResult(data: List<T>, position: Int, totalCount: Int) {
if (isInvalid) {
//......
} else {
val nextKey = position + data.size
resume(
params,
BaseResult(
data = data,
// skip passing prevKey if nothing else to load
prevKey = if (position == 0) null else position,
// skip passing nextKey if nothing else to load
nextKey = if (nextKey == totalCount) null else nextKey,
itemsBefore = position,
itemsAfter = totalCount - data.size - position
)
)
}
}
// ......
)
}
loadInitial
方法里面主要做了两件事:
- 调用另一个
loadInitial
方法获取数据。这个loadInitial
方法就是从数据库里面获取数据,有兴趣的同学可以看看,这里就不展开了。- 根据请求的结果,返回一个BaseResult。这里我们特别注意的是,当
nextKey == totalCount
时,返回nextKey,这个验证了我们之前的说法。
至此PagingSource的Refresh加载就结束了,这里我省略Append的过程分析,因为Append过程和Refresh过程非常相似,只不过他们在调用的方法不一样而已。Refresh调用的是loadInitial
方法,Append 调用的loadRange
方法,其他地方都比较类似的,这里就不过多的分析了。
在这里,我猜测大家心里面还有疑惑,PagingSource的加载(Refresh 和Append)还是不理解,这两个操作是怎么关联起来的呢?接下来,我将继续给大家解疑答惑。
4. PagingSource的Refresh和Append关联
在单一数据源中,我们都知道PagingSource一次完整的加载过程包括:一次Refresh + 多次Append + 多次Prepend。但是在多级数据源中却不是这样的,在多级数据源中,PagingSource完整加载过程是:[一次Refresh + 多次Append + 多次Prepend] + [一次Refresh + 多次Append + 多次Prepend]...... 。
注意,在多级数据源中,
RemoteMediator
的完整加载过程是:一次Refresh + 多次Append + 多次Prepend。PagingSource的完整过程不是这样的,这一点一定要明确。
上面已经简单的说明了PagingSource完整加载过程,在这里我们详细的解释一下。主要从两个方面来说:
- Refresh + Append:当Refresh一次之后,本地会预取一批(不只是一页数据,这里默认为pagSize大小的数据量为一页)的数据,我们通过向下滑动的操作会将预取的数据一页一页(即每次取pageSize大小的数据)的Append到UI层。当这次Refresh的数量被消费完毕,即滑动到边界(或者说,预取的数据已经被完全加载到UI层了),此时nextKey为会空,从而再次触发
RemoteMediator
的网络请求(此时RemoteMediator
的loadType是Append)。RemoteMediator
请求完成之后,更新到数据库中,触发器会通知PagingSource重新创建并且Refresh(不完全刷新),再开始一轮的Append。同理Prepend也是类似的。- Prepend:首先,这里
Prepend
说的不是从数据库获取新的数据,而是获取旧的数据。比如说,当前,我们向下滑动到200位置,在向上滑动,会进行Prepend操作(即从数据库获取之前的数据)。为啥会这样呢?因为多级数据源中,PagePresenter不会保存所有的数据(即打破了保存所有数据的规则,前面已经说过),最多会保留initialLoadSize
大小的数据。其他的数据都会用占位符来代替,即PagePresenter
里面的placeholdersBefore
和placeholdersAfter
,当再次需要使用的时候,会重新从数据库中加载并且显示。
上述第二点,我们从代码找到答案,就拿PositionalDataSource
的loadInitial
方法来说:
override fun onResult(data: List<T>, position: Int, totalCount: Int) {
if (isInvalid) {
// NOTE: this isInvalid check works around
// https://issuetracker.google.com/issues/124511903
cont.resume(BaseResult.empty())
} else {
val nextKey = position + data.size
resume(
params,
BaseResult(
data = data,
// skip passing prevKey if nothing else to load
prevKey = if (position == 0) null else position,
// skip passing nextKey if nothing else to load
nextKey = if (nextKey == totalCount) null else nextKey,
itemsBefore = position,
itemsAfter = totalCount - data.size - position
)
)
}
}
随着我们不断向下滑动,position会变得越来越大,因此itemsBefore
就会变得越来越大(即PagePresenter
的placeholdersBefore
)。但是,我们的有效数据总量不会变大,始终是initialLoadSize
这么多。因此,当我们使用RemoteMediator
时,不要尝试获取任意位置的数据,因为获取的有可能是空。
5. 使用RemoteMediator的一些小建议
至此,源码分析我们算是结束了,在这里,我对使用RemoteMediator
提一些小建议。
(1). 不要随意的调用Adapter的getItem方法获取任意位置的数据
因为PagePresenter
只会保留initialLoadSize
大小的有效数据,其他位置都会用null来填充,所以通过getItem
方法获取的数据很有可能为空,容易造成不必要的错误。
(2). 尽量将PageConfig的initialLoadSize设置的大一点
因为在滑动过程中,PagingSource的Append和Prepend操作消费的是RemoteMediator获取的initialLoadSize
大小的数据,将initialLoadSize
设置大一点,可以减少RemoteMediator
的请求。其次最好将initialLoadSize
设置为pageSize
整数倍,避免在Append和Prepend时出现断页的情况。
(3). 最好RemoteMediator每次请求的数量量都设置成为一样,且都为initialLoadSize
对于RemoteMediator
来说,每次请求虽然loadType不一样,但是本质都是差不多的,都是PagingSource发现数据不够了,需要新增数据。所以每次请求的数据都一样,能保证逻辑简单且统一。参考代码如下:
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Message>
): MediatorResult {
val startIndex = when (loadType) {
LoadType.REFRESH -> 0
LoadType.PREPEND -> return MediatorResult.Success(true)
LoadType.APPEND -> {
val stringBuilder = StringBuilder()
state.pages.forEach {
stringBuilder.append("size = ${it.data.size}, count = ${it.data.count()}\n")
}
Log.i("pby123", stringBuilder.toString())
count += state.config.initialLoadSize
count
}
}
Log.i("pby123", "CustomRemoteMediator,loadType = $loadType")
return try {
val messages = Service.create().getMessage(state.config.initialLoadSize, startIndex)
DataBaseHelper.dataBase.withTransaction {
if (loadType == LoadType.REFRESH) {
mMessageDao.clearMessage()
}
mMessageDao.insertMessage(messages)
}
MediatorResult.Success(messages.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
(4). RemoteMediator的load方法的PagingState存储的数据不是所有的数据
PagingState
内部存储的数据并不是所有的数据,而是上一次Refresh的数据,不要尝试通过这个变量来计算所有数据的总数。不过,可以通过如下代码计算,但是不能保证100%靠谱,因为itemsBefore
和itemsAfter
可能是无效值。
val pages = state.pages
var totalCount = 0
if(pages.isNotEmpty()){
pages.forEach {
totalCount += it.data.size + it.itemsBefore + it.itemsAfter
}
}
6. 总结
到这里,Paging3源码分析的内容就结束了,我做了一个简单的总结:
- 正常情况下,
RemoteMediator
只会Refresh一次,除非手动Refresh;PagingSource可能会多次Refresh,除了第一个初始化Refresh之外,当RemoteMediator
从网络上获取,放到数据库时,PagingSource也会Refresh。- 在Paging3里面,分为两种刷新,分别是不完全刷新,即调用
PageFetcher
的invalidate
方法,在这种情况下,本地数据库的数据不会清空,只会新增数据;完全刷新,即调用PageFetcher
的refresh
方法,此种刷新会清空本地数据库的数据。- 多级数据源中的PagingSource完整加载过程是:[一次Refresh + 多次Append + 多次Prepend] + [一次Refresh + 多次Append + 多次Prepend]......,这个跟单一数据源不一样。