Google爸爸在今年(2020年)的Jetpack库里面更新paging组件,推出了Paing3。按照Google爸爸文档的描述,Paing3完全使用的是kotlin,其中还包括了kotlin 的很多特性,比如说协程,Flow和Channel等。出于好奇,想要了解其使用方式和内部实现原理,因此,便写下这篇文章。
其实,Paging3的推出时间对于我来说挺尴尬,Paging3推出的第一个版本时,那个时候我在看Paging2的源码。当时在学习Paging2的时候,内心一直在想,我还有必要学习这个吗?不得不说,Google爸爸推陈出新的速度太快了,都来不及追随了。
好了,不废话了,开始本文的主题吧。本文打算由浅入深的分析Paging3,从最开始的基本使用讲到内部实现原理。主要内容如下:
- 基本使用。包括PagingSource的使用,RemoteMediator的使用。
- 基本架构。Paging3内部定义了很多的类,在分析原理之前,我们需要理解清楚每个类的含义,以及类之间是怎么串联起来。这些理解清楚了,看代码的时候才不会疑惑。
- 首次加载的过程。主要是分析Paging怎么实现第一次Refresh操作的。
- 加载更多的过程。主要是分析Pagin怎么去加载上一页或者下一页的数据。
- RemoteMediator的实现原理。了解Paging的同学应该都知道,RemoteMediator主要是将网络上的数据放在缓存在数据库里,以保证无网的状态下也能加载数据。由于RemoteMediator的实现原理跟普通单一数据源加载方式有很大不同的,所以我单独拎出来分析。(下篇内容)
同时介于篇幅的原因,将本文的内容拆分上下两篇,本文是上篇内容,下篇内容主要讲解RemoteMediator
。
在阅读本文之前,默认大家已经掌握如下的知识,本文不做单独介绍:
- Kotlin 的协程,Flow和Channel。
- Room的基本使用。
本文参考资料:
注意,本文Paging源码均来自于3.0.0-alpha08版本。
1. 概述
Paging组件本身的作用本文就不做过多的介绍,有兴趣的同学可以看看:Jetpack 源码分析(四) – Paging源码分析。在这里,我介绍一下Paging3和Paging2的不同。
- Adapter从
PagedListAadpter
更换成为了PagingDataAdapter
。- 废弃了
PagedList
,内部使用PagingData
和PagePresenter
来存储。网上有人说PagingData
替代了PagedList
,我觉得不太准确。因为在Paging2中,PagedList只会创建一次,那就是在Refresh的时候,同时PagedList还兼有数据存储和处理,存储了已加载所有的数据;而在Paging3里面,PagingData只存储一次Refresh + 多次Prepend + 多次Append的数据,之所以这么说,因为在Paging3中,即使不手动Refresh,也会可能会Refresh,这个主要是跟RemoteMediator
有关。所以在Paging3
中,PagingData
只是用来提交数据,而不是存储数据的,存储和处理数据主要是由PagePresenter
。- 简化了数据加载的逻辑。在Paging2中,我们通过自定义DataSource来实现加载,定义的时候需要考虑初始化加载,以及更多加载的区别,同时还需要实现不同DataSource,来区分不同场景的数据;而在Paging3中,只需要定义
PagingSource
负责加载数据即可。注意,我们不能理解为DataSource已经过时了,一是Google爸爸并没有把这个类标记为过时,其次在RemoteMediator
内部还在使用它。
Paging3内部实现完全使用Kotlin实现,其中使用Flow和Channel替代了LiveData,通过协程实现异步处理。作为使用者,我喜欢这种设计,因为更加简洁;但是,做了开发者和源码阅读者来说,增加很多的工作量。从两个方面来说:
- 使用Flow和Channel实现监听。Flow的上游来源在下游是不可知的,如果下游出现问题,没法追溯,只能愣看代码。
- 使用协程进行异步处理。debug难度增加,就目前而言,debug Paging3内部的源码存在两个问题,要么打了debug的断点根本不生效,要么就是虽然生效了,但是始终断不下来。
例如,打了debug的断点根本不生效。我想看一下PageFetcherSnapshot
的doLoad
方法调用情况,发现打了断点根本不生效: 在另外一个地方打了断点,虽然生效了,但是根本断不下来: 大家看上面动图,我在前后打了两个断点,前面那个断点不能断,后面那个断点可以断下来。真的,看Paging3的源码真的太难了,连普通的debug都成问题。
不过,抱怨归抱怨,源码我们还是要看的。
2. 基本使用
在分析原理实现之前,我们先来看看Paging3的基本使用。Paging的使用主要分为两个部分:单一数据源和多级数据源。
(1). 单一数据源
在Paging3中,PagingSource
就是负责单一数据源的加载。那么什么是单一数据源呢?我的理解是,只有一个来源的数据就是单一数据源,比如说我从PagingSource中获取数据,只能来自于一个地方,要么网络,要么本地,这个就是所谓单一数据源。我们来看看,是怎么使用的吧,主要分为三步:
- 定义一个
PagingSource
,并且使用ViewModel 将提供给Activity/Fragment。- 自定义一个RecyclerView的Adapter,继承于
PagingDataAdapter
- Activity/Fragment从ViewModel获取一个Flow,并且监听,同时将监听的到内容通过Adapter的
submitData
方法提交给它。
我们来看看具体的代码实现。
A. 定义PagingSource
我们直接看代码:
class CustomPagingSource : PagingSource<Int, Message>() {
private var count = 1
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
return try {
val message = Service.create().getMessage(params.pageSize)
LoadResult.Page(message, null, count++)
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
}
相比于Paging2(Paging2里面是DataSource),Paging3对PagingSource的定义就变得非常简单了,只需要我们实现一个方法就行了,不过这里面我们需要注意两个点:
- load方法的返回值是一个LoadResult对象。其中如果请求成功的话,需要返回
LoadResult.Page
;如果请求失败,需要返回LoadResult.Error
对象,这一点大家一定要注意。在这里,我特意的分析一下LoadResult.Page
内部几个参数含义,其内部几个参数含义如下:
参数 | 含义 |
---|---|
data | 加载的数据列表。 |
prevKey | 上一页数据的key。如果为空的话,表示没有上一页数据。 |
nextKey | 下一页数据的key。如果为空的话,表示没有下一页数据。 |
itemsBefore | 当前加载完毕页之前需要加载占位符的数量,默认值是COUNT_UNDEFINED ,表示不会创建占位符。比如说,当前加载的是第一页的数据,itemsBefore为100的话,那么在这页数据之前,会有100个占位符,这个我们也可以从Adapter的itemCount验证,这里就不展示。同时,这里也不解释什么是占位符,感兴趣的同学可以参考:Jetpack 源码分析(四) – Paging源码分析 |
itemsAfter | 同itemsBefore ,表示当前加载完毕页之后需要加载占位符的数量。 |
2. 在加载数据完成之后返回
LoadResult.Page
,注意设置prevKey
和nextKey
的值,否则有可能不会自动加载下一页的数据。
B. 定义ViewModel并且暴露Flow
在Paging3中,Google爸爸推荐使用Flow替代LiveData,用来给UI层传递数据。这里为了简单起见,我们就不独树一帜,按照Google爸爸推荐来定义。看看代码:
class NetWorkViewModel : ViewModel() {
val messageFlow = Pager(PagingConfig(20)) {
CustomPagingSource()
}.flow.cachedIn(viewModelScope)
}
如上便是ViewModel定义的完整过程,非常的简单,不过这里我们需要注意如下:
- Flow是从Pager里面拿到的,其中它有几个参数,我们需要注意一下,如下:
参数 | 含义 |
---|---|
config | Paging 的配置项,这个配置项我们在Paging2里面已经解释过了,这里就不过多的解释,只解释新增几个配置参数。initialLoadSize ,表示初始化加载需要加载的数据量,默认是pageSize * 3,针对于这个参数我们特别要跟pageSize 区分开来,虽然我们设置了pageSize ,但是如果没有设置initialLoadSize ,第一次加载的数据量是pageSize * 3 ;jumpThreshold ,根据Google爸爸注释和实现的代码,大概的意思是,当滑动到了边界,并且加载的数据量超过了设置的阈值,那么就触发refresh逻辑,此字段我会在分析RemoteMediator 里面分析。 |
initialKey | 初始加载的key。 |
remoteMediator | 多级数据源请求的工具类,这个类的意思后续我会专门的介绍,这里先不介绍了。 |
pagingSourceFactory | 用来创建一个PagingSource。 |
2. 我们将flow对象,通过
cachedIn
方法缓存在一个viewModelScope
里面。根据Google爸爸的介绍,这样可以在Activity重建的时候,Flow里面已经转换过的数据不会再次转换,而是直接拿来用。
如上便是ViewModel的定义,是不是很简单了呢?接下来,我们再来看看View层是怎么监听的。
C. View层监听Flow
首先View层要想监听Flow的回调,RecyclerView 的Adapter必须继承于PagingDataAdapter
。关于Adapter的定义,这里就不单独的讲解,相信大家都非常的熟悉。
我们直接来看代码:
lifecycleScope.launchWhenCreated { mMessageFlow.collectLatest { adapter.submitData(it) } }
上面展示的是Adapter监听Flow的数据变化,其中,我们需要注意如下的内容:
collectLatest
方法是挂起函数,所以必须在协程里面调用。lifecycleScope
是Google爸爸给我们提供生命周期敏感的协程作用,保证页面销毁时协程能正常取消。
(2).多级数据源
单一数据源比较简单,数据来源只有一个地方。实际上,在真实项目开发过程中,数据来源并没有那么简单,很常见的场景是:本地数据库 + 网络数据。如果手机没有网络,可以使用本地数据库显示数据,以保证用户在无网络的时候能正常使用App。
出于这种情况的考虑,Google爸爸在设计Paging3时,新增加了一个工具类RemoteMediator
,用来实现多级数据源。我们来看看具体代码实现(如下代码涉及到Room库,这里就不单独解释,默认大家都懂)。
首先使用Room定义一个Dao,用以对数据库操作,通常来说Dao里面必须含有三个操作
- 查询,需要返回一个
PagingSource
对象。- 插入,当有新数据,我们需要同步到数据库中去,以备后用。
- 清空所有数据,当用户刷新操作成功之后,应该清空之前所有的数据,然后才插入新的数据。
@Dao
interface MessageDao {
@Query("select * from message")
fun getMessage(): PagingSource<Int, Message>
@Insert( onConflict = OnConflictStrategy.REPLACE )
fun insertMessage(messages: List<Message>)
@Query("delete from message")
fun clearMessage()
}
Dao的定义便是如上,大家需要注意的是,查询操作返回的是一个PagingSource对象。如果大家发现Room生成的代码不支持PagingSource
类型,可以将Room升级到2.3.0版本。
其次就是实现RemoteMediator
接口,用以从网络获取数据,我们直接来看具体的实现:
class CustomRemoteMediator : RemoteMediator<Int, Message>() {
private val mMessageDao = DataBaseHelper.dataBase.messageDao()
private var count = 0
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 += 20
count
}
}
val messages = Service.create().getMessage(20, startIndex)
DataBaseHelper.dataBase.withTransaction {
Log.i("pby123", "loadType = $loadType")
if (loadType == LoadType.REFRESH) {
mMessageDao.clearMessage()
}
mMessageDao.insertMessage(messages)
}
return MediatorResult.Success(messages.isEmpty())
}
}
RemoteMediator
的定义跟PagingSource
比较类似,都有load方法,只不过他们所做的事情不太一样,RemoteMediator
的load方法做了两件事:
- 通过不同的loadType获取key,这个key可能是prevKey,也可能是nextKey。
- 通过拿到的key从网络上请求数据,并且放到数据库中去。需要注意的是,这里并并没有将请求的数据结果通过result返回去,那么是因为
RemoteMediator
的职责是从网络上请求数据,然后放到数据库里面,这一点跟PagingSource
有很大的不同。那么RemoteMediator
请求回来时怎么提交给到Adapter呢?这个我们在后续的内容会重点分析。
最后就是ViewModel里面的定义,我们直接来看代码:
class NetWorkAndDataBaseViewModel : ViewModel() {
@ExperimentalPagingApi
val messageFlow = Pager(PagingConfig(20), remoteMediator = CustomRemoteMediator()) {
DataBaseHelper.dataBase.messageDao().getMessage()
}.flow.cachedIn(viewModelScope)
}
多级数据源跟单一数据源关于Flow的创建不太一样,主要体现在如下两点:
- Pager的构造方法里面需要传入一个
RemoteMediator
对象,这个就是我们自定义的RemoteMediator
。- 其次,PagingSource不需要我们自定义,直接从我们之前定义的Dao里面获取就行。Room会通过代码生成的方式,返回一个指定的PagingSource对象。
关于多级数据源的其地方都跟单一数据源都是一样的,这里就不再赘述了。
(3). 代码
为了大家方便理解,我将我的Demo代码上传到github:KotlinDemo。同时额外的说一句,后续我会将所有的Demo代码汇总在这个工程里面,方便大家参考学习。
3. 基本架构
接下来,我们将进入源码分析阶段。不过在这之前,我们先来了解一下Paging3内部整个架构,方便后续在源码分析的时候,脑海中先有一个轮廓,不至于懵逼。
根据我对Paging3框架的理解,我将Paging3内部实现分为两个部分:
- 数据请求层。这一层主要负责的是数据请求,其中包括加载初始化的数据,加载更多的数据,以及多级数据源的请求。这部分的内容主要是由
PageFetcher
,PageFetcherSnapshot
,PagingSource
,RemoteMediator
等组成。- 数据处理和显示层。这一层主要是负责拿到数据请求层请求回来的数据,然后进行处理和显示。这部分的内容主要是由
PagingDataAdapter
,AsyncPagingDataDiffer
,PagingDataDiffer
和PagePresenter
我们来分开看一下每一层主要架构和联系。
(1). 数据请求层
数据请求层最主要的责任就是数据请求,而数据请求的类型在Paging3分为两种:
- 初始化页面数据请求。
- 上一页或和下一页数据请求。
这其中,由PageFetcher
提供Api触发请求,比如说PageFetcher
里面有refresh
和invalidate
两个方法可以触发刷新逻辑;而更多数据请求是通过PageFetcher
的PagerUiReceiver
来首先触发。整体逻辑是:由PageFetcher
内部的Flow创建一个PageFetcherSnapshot
对象,数据请求的操作通过PageFetcher
传递到PageFetcherSnapshot
里面,PageFetcherSnapshot
内部有两个方法来请求数据,分别是:
- doInitialLoad:表示加载刷新的数据。
- doLoad:表示加载其他页的数据。
所有的数据请求都会走到这两个方法进行数据请求,PagingSource
和RemoteMediator
的load方法也是在这两个方法里面进行调用的。
(2). 数据处理和显示层
刷新请求完成之后,PageFetcher
会通过内部的Flow发送一个PagingData
对象。而Adapter会通过监听拿到这个PagingData,然后进行数据处理。这其中PagingData内部封装了几个参数:
PageEvent
:内部封装了关于数据的信息。包括当前加载类型,即LoadType
(REFRESH,PREPEND,APPEND);加载状态,即CombinedLoadStates
(NotLoading,Loading,Error);以及更重要的数据列表。UiReceiver
:用来触发加载其他页面数据的接口。
Adapter拿到这个PaingData
对象之后,会一路透传,直到PagingDataDiffer
的collectFrom
方法。在PagingDataDiffer
内部,首先会通过PagingData
内部的一个Flow监听PageEvent,然后不同类型的PageEvent(Insert,Drop,LoadStateUpdate)进行分发,如果是LoadStateUpdate
那么表示是加载状态更新的,即会回调加载状态的监听Listener;如果是其他类型的PageEvent就进行数据处理,数据处理主要是依靠PagePresenter来帮忙,然后将对应的数据变化同步到Adapter层面上,即调用Adapter的notify方法。
(3). 枚举类
在Paging3的内部,有很多的枚举类,用来表示某种状态。如果我们在看源码对这些枚举类不了解,那么代码理解起来就比较麻烦,所以我在这里重点解释一下。
A.LoadType
即加载类型,一共有三个枚举类,具体名字和含义如下:
名字 | 含义 |
---|---|
REFRESH | 刷新 |
PREPEND | 表示加载上一页数据 |
APPEND | 表示加载下一页数据 |
B.LoadState
加载状态,即当前加载是什么一个情况,通常来说,每一种LoadType都对应一个LoadState,表示当前加载类型的具体状态。具体名字和含义如下:
名字 | 含义 |
---|---|
NotLoading | 表示没有在加载或者已经加载完成。内部带有一个标记字段,即endOfPaginationReached ,用来表示当前是否还有剩余的数据需要加载,其中false表示还有剩余的数据未加载,true表示没有剩余的数据。比如说APPEND的状态为NotLoading,表示当前加载更多已经完成,如果endOfPaginationReached 为false,表示还有数据需要加载,到了合适的时机还有触发加载更多,反之亦然。 |
Loading | 表示正在加载。 |
Error | 表示加载失败。 |
C.PageEvent
最终数据请求的结果都会作为PageEvent
通过PagingData
传递到UI层,PageEvent一共三个枚举状态,分别如下:
名字 | 含义 |
---|---|
Insert | 表示有新的数据增加,其中Refresh ,Append ,Prepend 请求成功之后都会产生这个事件。 |
Drop | 表示有数据需要删除,这个事件是在Append 和Prepend 时机才会产生。 |
LoadStateUpdate | 表示每种加载类型的加载状态更新。 |
除去这些枚举类,Paging3内部还有各种Helper类,用来存储状态,这里就不再过多的介绍,在源码分析过程中,如果遇到,我会进行简单的解释。
4. 首次加载
接下来,我们将进入源码分析阶段,首先我们来看看首次加载的过程,需要注意的是这里的首次加载泛指进入页面的第一次加载和手动刷新加载。这里我从两个方面分析首次加载的过程,分别是:数据请求层和数据处理和显示层。首先我们来看一下数据请求层的实现。
(1). 数据请求层
我们在介绍基本使用的时候已经提到过,我们需要通过Pager拿到一个Flow对象,Pager的Flow对象其实是从PageFetcher
里面获取的,所以我们直接来看PageFetcher
里面实现。
在正式介绍源码之前,我们先来看一下PageFetcher
内部的两个Channel对象:
refreshChannel
:用来通知触发刷新逻辑。其中true表示RemoteMediator
也要刷新(如果有的话),false则表示RemoteMediator
不刷新。retryChannel
:用重试刷新。我们通过Adapter的retry方法,最终会通过这个Channel来通知重试。
接下来,我们来看一下PageFetcher
内部最重要的一个Flow的实现:
val flow: Flow<PagingData<Value>> = channelFlow {
val remoteMediatorAccessor = remoteMediator?.let {
RemoteMediatorAccessor(this, it)
}
refreshChannel.asFlow()
.onStart {
@OptIn(ExperimentalPagingApi::class)
emit(remoteMediatorAccessor?.initialize() == LAUNCH_INITIAL_REFRESH)
}
.scan(null) {
previousGeneration: PageFetcherSnapshot<Key, Value>?, triggerRemoteRefresh ->
var pagingSource = generateNewPagingSource(previousGeneration?.pagingSource)
while (pagingSource.invalid) {
pagingSource = generateNewPagingSource(previousGeneration?.pagingSource)
}
@OptIn(ExperimentalPagingApi::class)
val initialKey: Key? = previousGeneration?.refreshKeyInfo()
?.let { pagingSource.getRefreshKey(it) }
?: initialKey
previousGeneration?.close()
PageFetcherSnapshot<Key, Value>(
initialKey = initialKey,
pagingSource = pagingSource,
config = config,
retryFlow = retryChannel.asFlow(),
// Only trigger remote refresh on refresh signals that do not originate from
// initialization or PagingSource invalidation. triggerRemoteRefresh = triggerRemoteRefresh,
remoteMediatorConnection = remoteMediatorAccessor,
invalidate = this@PageFetcher::refresh
)
}
.filterNotNull()
.mapLatest { generation ->
val downstreamFlow = if (remoteMediatorAccessor == null) {
generation.pageEventFlow
} else {
generation.injectRemoteEvents(remoteMediatorAccessor)
}
PagingData(
flow = downstreamFlow,
receiver = PagerUiReceiver(generation, retryChannel)
)
}
.collect { send(it) }
}
初次看这段代码,可能会有点懵逼,最初我也是这样的。不过,大家不用担心,我会给大家介绍这个Flow里面做了哪些事情。我们先从宏观上来看这段代码都做啥事吧(这里为了简单,我们先把RemoteMediator
相关的忽略,后续有内容专门来介绍它。),分别:
- 在scan方法里面,创建了
PageFetcherSnapshot
对象。主要分为三步:首先,通过传入进来的工厂函数创建一个PagingSource,同时如果还有之前的PageFetcherSnapshot
存在,需要进行一些清理工作(scan方法内部有一个size 为1的Buffer,会缓存上一个PageFetcherSnapshot
对象);其次调用refreshKeyInfo
方法拿到刷新的key;最后就是创建了一个PageFetcherSnapshot
,同时给PageFetcherSnapshot
传入可能会用到的参数。- 通过map函数将相关事件转为成为一个
PagingData
对象,同时还有PagingData传入两个参数,分别是一个PageEvent的Flow对象,下游(UI 层)可以用来监听PageEvent 的发送;其次,就是构建了一个PagerUiReceiver
对象,用来给下游(UI 层)来触发加载下一页数据和尝试重试加载。- 通过send方法将创建好的PagingData发送出去的。
我们都知道,Flow是冷流,即只有在收集的时候才会触发上面一系列的流程。所以我们在Activity/Fragment 里面调用Flow的collectLatest
方法自然触发了上面流程,从而开始初始化加载。
关于上面的代码,大家还有需要注意一点的是,PagingData的PageEvent是从PageFetcherSnapshot
获取的,我们在前面介绍过,PageFetcherSnapshot
的工作主要负责加载数据,同时将加载完成的数据通过发送一个PageEvent来通知到下游。
我们接下来看一下PageFetcherSnapshot
的pageEventFlow
参数,因为所有的事件都是通过它发送到下游去的。
val pageEventFlow: Flow<PageEvent<Value>> = cancelableChannelFlow(pageEventChannelFlowJob) {
// Start collection on pageEventCh, which the rest of this class uses to send PageEvents
// to this flow.
launch {
pageEventCh.consumeAsFlow().collect {
// Protect against races where a subsequent call to submitData invoked close(),
// but a pageEvent arrives after closing causing ClosedSendChannelException.
try {
send(it)
} catch (e: ClosedSendChannelException) {
// Safe to drop PageEvent here, since collection has been cancelled.
}
}
}
// ......(retry 加载)
// ......(Remote Mediator Refresh)
// Setup finished, start the initial load even if RemoteMediator throws an error.
doInitialLoad(state)
// ......(监听下游发送的过来的事件,尝试加载上一页或者下一页刷剧)
}
pageEventFlow
的代码较多,我删除一些我们现在不用关心的代码,避免影响我们分析整个流程。我们从上面已有的代码,我们看出来几点:
- pageEventCh是用来发送PageEvent的,这里发送的PageEvent会直接到达UI 层。不过需要注意的是,这里发送的PageEvent不仅仅是Insert,还有其他类型的(Drop和LoadStateUpdate)。
- 调用了
doInitialLoad
方法,进行数据请求。
接下来我们来看doInitialLoad
方法的实现。先直接看代码:
private suspend fun doInitialLoad( state: PageFetcherSnapshotState<Key, Value> ) {
// 设置状态,当前正在刷新。
stateLock.withLock { state.setLoading(REFRESH) }
val params = loadParams(REFRESH, initialKey)
// 调用pagingSource的load 方法,进行网络请求
when (val result = pagingSource.load(params)) {
is Page<Key, Value> -> {
// 将请求的结果插入到PageFetcherSnapshotState里面
val insertApplied = stateLock.withLock { state.insert(0, REFRESH, result) }
// 更新loadType 对应的loadState
stateLock.withLock {
state.setSourceLoadState(REFRESH, NotLoading.Incomplete)
if (result.prevKey == null) {
state.setSourceLoadState(
type = PREPEND,
newState = when (remoteMediatorConnection) {
null -> NotLoading.Complete
else -> NotLoading.Incomplete
}
)
}
if (result.nextKey == null) {
state.setSourceLoadState(
type = APPEND,
newState = when (remoteMediatorConnection) {
null -> NotLoading.Complete
else -> NotLoading.Incomplete
}
)
}
}
// 通知UI层进行数据已经更新。
if (insertApplied) {
stateLock.withLock {
with(state) {
pageEventCh.send(result.toPageEvent(REFRESH))
}
}
}
// ......(remoteMediator的调用,先忽略)
}
is LoadResult.Error -> stateLock.withLock {
val loadState = Error(result.throwable)
if (state.setSourceLoadState(REFRESH, loadState)) {
pageEventCh.send(LoadStateUpdate(REFRESH, false, loadState))
}
}
}
}
doInitialLoad
方法所做事情比较简单,我们来看一下:
- 首先调用
PageFetcherSnapshotState
的setLoading方法表示当前正在刷新,在setLoad方法里面会通过pageEventCh
发送一个LoadStateUpdate
事件,来通知UI 层加载状态已经变化了。- 调用PagingSource的load 方法,进行数据请求。这个数据可能是从网络上请求数据,也有可能是从数据库里面请求数据,具体得看PagingSource的定义。
- 根据
load
返回的结果进行分情况讨论。如果是Error
,那么就会给下游发送请求失败的PageEvent;如果是请求成功,即返回的是Page
,那么就分为几步来进行。首先是,将请求的结果存储到PageFetcherSnapshotState
里面去;其次返回结果传入的nextKey和prevKey来更新每个LoadType下的LoadState,以保证后续的加载更多能够正常进行。- 发送一个PageEvent,通知UI层数据更新。
doInitialLoad
方法所做之事便如上内容,在这里,大家发现了一个PageFetcherSnapshotState
类,肯定有疑惑这个类是干嘛的,我在这里简单的解释一下。
通过官方的注释,我们可以知道这个类主要是来记录PageFetcherSnapshot
的状态,那么记录都是什么状态呢?
- 数据相关的信息。
PageFetcherSnapshotState
内部有一个_pages
变量,记录的是已经加载的页面数据,这个我们从doInitialLoad
方法里面也可以看到,请求完成之后会调用PageFetcherSnapshotState
的方法
,目的就是将数据存储到PageFetcherSnapshotState
。还有其他数据相关信息,比如说当前占位符的数量,即placeholdersBefore
和placeholdersAfter
,这个变量跟我们之前在介绍LoadResult.Page
的itemsBefore
和itemsBefore
是同一个意思。以及还有其他信息,这里就不过多的介绍。- 每种LoadType对应的LoadState。内部有一个
sourceLoadStates
变量,记录三种LoadType 的状态。外部通常通过setSourceLoadState
来更新对应的值,同理,我们可以在doInitialLoad
方法看到它被调用的影子。
(2). 数据处理和显示层
我们从上面知道了首次加载的数据会通过发送一个PageEvent传送到数据处理和显示层(即UI 层,为了简单,后文统一使用UI层表示)。
繁琐的源码追踪工作,我们这里不做了,我们直接到PagingDataDiffer
的collectFrom
方法里面去,因为在方法里面对PageEvent事件进行监听。我们直接看代码:
suspend fun collectFrom(pagingData: PagingData<T>) = collectFromRunner.runInIsolation {
// 存下UiReceiver,以备后续触发加载更多。
receiver = pagingData.receiver
pagingData.flow.collect { event ->
withContext<Unit>(mainDispatcher) {
// 如果是Insert,并且是刷新操作。
if (event is PageEvent.Insert && event.loadType == REFRESH) {
lastAccessedIndexUnfulfilled = false
// 创建一个PagePresenter用以存储和处理数据
val newPresenter = PagePresenter(event)
// 将旧PagePresenter里面数据迁移到新的PagePresenter
val transformedLastAccessedIndex = presentNewList(
previousList = presenter,
newList = newPresenter,
newCombinedLoadStates = event.combinedLoadStates,
lastAccessedIndex = lastAccessedIndex
)
presenter = newPresenter
// 回调Listener
dataRefreshedListeners.forEach { listener ->
listener(event.pages.all { page -> page.data.isEmpty() })
}
dispatchLoadStates(event.combinedLoadStates)
// 尝试触发加载下一页(上一页)的数据
transformedLastAccessedIndex?.let { newIndex ->
lastAccessedIndex = newIndex
receiver?.accessHint(
newPresenter.viewportHintForPresenterIndex(newIndex)
)
}
} else {
// Append 或者Prepend
}
}
}
}
collectFrom
方法的代码比较长,主要是处理两部分的内容:Refresh和非Refresh。这里,我们将非Refresh的代码省略,只看Refresh部分的代码。collectFrom
方法针对Refresh做了如下几件事:
- 存下UiReceiver,以备后续触发加载更多。这个我们说了很多遍,这里就不过多的说了,后续在介绍加载更多的过程时,会再次看到的。
- 创建一个新的新PagePresenter,用来存储和处理数据。
- 调用
presentNewList
方法,将旧的Presenter数据迁移到新的Presenter。主要实现是通过DiffUtil来计算Diff,进而通知Adapter notify,有兴趣的同学可以看看方法的实现,这里就不介绍了。同时看到这个,可能有人会有疑惑,Refresh都是将原来的数据清空,然后插入新的数据,为啥要把老的数据迁移到新的数据里面呢?在平常中,我们对Refresh的理解是没有问题的,但是在Paging3中,Refresh 操作不一定会清空老的数据,这一点一定记住。- 回调对应的Listener。
- 尝试触发加载下一页(下一页数据)的数据。
transformedLastAccessedIndex
表示将在老的数据里面的lastAccessedIndex
(上一次访问的位置,在get方法记录的)在新的数据中的位置。如果当前数据量还不够当前访问,需要加载更多的数据以满足要求。
相信大家在这里看到一个新的类–PagePresenter
,想要知道这个类的作用是什么?我在这里简单的解释一下。
PagePresenter内部存储了Adapter所有的数据,可以简单的理解为时Adapter的Data List。因为从源码中中看出来,Adapter 获取数据和计算当前数据总数量都是从通过PagePresenter。同时,PagePresenter 还担任着处理PageEvent的责任,因为内部有一个
processEvent
方法,这个方法的作用根据不同的PageEvent进行不同的处理,其中Insert
表示要新增数据;Drop
表示要删除数据;LoadStateUpdate
表示要更新状态。除此之外,这个类还有很多有意思的东西,比如说ViewportHint
的构造等,这里就不过多的介绍了。
UI层对Refrsh处理的过程便是如上的内容。到此,对首次加载的整个流程的分析就结束了,在这里,我做一个小小的总结,方便大家脑海中能把这部分的内容的串起来。
首先,在UI层,我们在从ViewModel 拿到一个Flow,这个Flow对象是用来监听PagingData。正常来说,只有Refresh才会发送一个PagingData,Append 和Prepend 不会发送PagingData。
PagingDataDiffer
会从PagingData里面拿到一个发送PageEvent
的Flow,当数据请求完成,这个Flow会收到一个Insert
类型的事件,这个事件里面封装了请求回来的数据。拿到这个PageEvent,会创建一个PagePresenter来存储和处理数据,以及处理对应的PageEvent。 其次,在数据请求层,PageFetcherSnapshot
通过调用doInitialLoad
方法,进而调用PagingSource
的load方法。load方法返回了一个Result,PageFetcherSnapshot
会将这个Result转换成为一个PageEvent,发送给UI层。
5. 加载更多
接下来,我们将分析另一个加载的过程–加载更多。为了简单起见,本章节的内容中以加载下一页的数据表示加载更多,即Append操作。
其实,我们在分析首次加载的过程中,已经涉及到了很多这部分的内容,当然在这之前也留下很多的伏笔。这里,我们就来详细的看一下加载更多的内容
在首次加载中,我们是先介绍数据请求层的内容,再介绍UI层的内容。在本章节中,我们反向操作,先介绍UI层的内容,再介绍数据请求层的内容。为啥要这样呢?因为首次加载是一个主动过程,不需要让UI层自己触发(严格来说,也是UI层自己触发的),而加载更多确实是被动过程,需要Ui层自己去触发。
(1). UI 层触发
总的来说,触发加载更多的地方很少,很简单,我将其分为两类:
- 调用Adapter的
getItem
方法,会尝试加载更多的数据。其中这个getItem方法的调用包括RecyclerView 自己调用,还有一个就是我们手动调用。所以,如果使用Paging3,不要随意的调用getItem方法,切记切记!- 正在请求的数据已经回来,但是发现已有的数据不够访问位置的要求,会自己请求。比如说,当前我们访问的index是100,数据请求回来发现才80条数据,还不够数量,会继续请求。
关于第一点,我不用解释什么。但是第二个点,我们需要重点分析一下,了解它的细节。在PagingDataDiffer
内部有两个变量:
- lastAccessedIndex:表示上一次访问的位置。
- lastAccessedIndexUnfulfilled:表示上一次访问是否命中占位符。通俗来讲就是,上一次访问的位置是否超出已有数据的边界,true表示超出边界,false表示没有超出边界。但是从源码来看,Google爸爸在
PagingDataDiffer
的get方法里面直接设置为true,所以可以理解这个变量应该默认每次访问都会超出边界。不过这样让人很疑惑,不知道Google是出于什么考虑。
当数据请求回来之后(包括Refresh和非Refresh),会根据上一次的访问位置会再次询问是否还可以加载下一页的数据。代码如下:
suspend fun collectFrom(pagingData: PagingData<T>) = collectFromRunner.runInIsolation {
// ······
pagingData.flow.collect { event ->
withContext<Unit>(mainDispatcher) {
if (event is PageEvent.Insert && event.loadType == REFRESH) {
// ······
transformedLastAccessedIndex?.let { newIndex ->
lastAccessedIndex = newIndex
// 尝试请求下一页数据
receiver?.accessHint(
newPresenter.viewportHintForPresenterIndex(newIndex)
)
}
} else {
// ·······
if (!canContinueLoading) {
// ·······
} else if (lastAccessedIndexUnfulfilled) {
// ·······
if (shouldResendHint) {
// 尝试请求下一页数据
receiver?.accessHint(
presenter.viewportHintForPresenterIndex(lastAccessedIndex)
)
} else {
// lastIndex fulfilled, so reset lastAccessedIndexUnfulfilled.
lastAccessedIndexUnfulfilled = false
}
}
}
}
}
}
}
关于这里面的计算细节,我们就不细扣了,有兴趣的同学可以去看看。不过这里,我们发现在调用UiReceiver
的accessHint
方法时,创建了一个ViewportHint
对象,那么这个ViewportHint
对象有什么用呢?我们先来看一下这个类的几个成员变量:
名字 | 含义 |
---|---|
pageOffset | 表示当前访问index所在的页面index。我们都知道,Paging里面的数据都是分页存储,类似于List<List>结构,而这个pageOffset 表示的时List的index。 |
indexInPage | 表示当前访问index在页面内的index。 |
presentedItemsBefore | 表示当前访问index之前非展位符的数量。比如说,当前访问位置是100,当时前置占位符有40个,那么presentedItemsBefore 就等于60(100 – 40)。同时如果这个变量如果为0,表示访问位置恰好是上边界;如果是正数,那么表示访问位置正好在数据范围内;如果是负数,访问位置就是占位符。 |
presentedItemsAfter | 同presentedItemsBefore ,只是presentedItemsAfter 表示的是下边界。 |
originalPageOffsetFirst | 当前数据第一页面的页码。不一定都为0,因为有maxSize 的存在,maxSize会丢弃前面已失效的数据(用null来填充)。 |
originalPageOffsetLast | 同originalPageOffsetFirst ,只是originalPageOffsetLast 表示最后一页的页面。 |
关于上面的解释,我猜测有些同学可能还会疑惑,我在这里在补充几句。
在Paging3内,存在三种index,分别是:
- 绝对index,我们可以这样来理解这个index,就是将所有的数据拍平在一个List里面,每个item的Index就是绝对index。
- 页面index,顾名思义,就是该页面数据在所有页面的index。上述的
pageOffset
,originalPageOffsetFirst
,originalPageOffsetLast
都是页面index。- 页内Index(相对index),就是指该Item所在页面里面内的index。上述的
indexInPage
便是页内index。
(2). 数据请求层。
UI层通过调用UiReceiver
的accessHint
方法,并且通过一个ViewportHint
对象来携带一些信息。而accessHint
方法便是Ui层和数据请求层的沟通桥梁,数据请求层监听到这个方法的回调,会向一个名为hintChannel
通道发送一个事件:
fun accessHint(viewportHint: ViewportHint) {
lastHint = viewportHint
@OptIn(ExperimentalCoroutinesApi::class)
hintChannel.offer(viewportHint)
}
注意上述的
accessHint
方法在PageFetcherSnapshot
里面,调用关系:PagerUiReceiver#accessHint
->PageFetcherSnapshot#accessHint
。
那么哪里在监听这个事件呢?是在startConsumingHints
方法里面。不过在看这个方法之前,我们先回过头来看一下pageEventFlow
的定义,之前我们看的时候省略了加载更多的代码。
val pageEventFlow: Flow<PageEvent<Value>> = cancelableChannelFlow(pageEventChannelFlowJob) {
// ······
// 加载更多
if (stateLock.withLock { state.sourceLoadStates.get(REFRESH) } !is Error) {
startConsumingHints()
}
}
通过上面的代码,我们可以看出来,实际上在进行Refresh操作的时候,就已经在监听加载更多操作,当然这个实现我们也可以猜想得到的,这里只不过验证一下具体实现而已。我们来看startConsumingHints
:
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
private fun CoroutineScope.startConsumingHints() {
// 1. 尝试触发Refresh操作
if (config.jumpThreshold != COUNT_UNDEFINED) {
launch {
hintChannel.asFlow()
.filter { hint ->
hint.presentedItemsBefore * -1 > config.jumpThreshold ||
hint.presentedItemsAfter * -1 > config.jumpThreshold
}
.collectLatest { invalidate() }
}
}
// 2. 加载上一页的数据
launch {
state.consumePrependGenerationIdAsFlow()
.collectAsGenerationalViewportHints(PREPEND)
}
// 3. 加载下一页的数据
launch {
state.consumeAppendGenerationIdAsFlow()
.collectAsGenerationalViewportHints(APPEND)
}
}
startConsumingHints
方法的实现很简单,但是这里有很多的细节需要注意:
- 这里有一个
jumpThreshold
,关于这个变量的含义,我们之前已经说过了,具体就是当前访问的数据已经超过了预先设置的阈值,那么直接就用Refresh操作来加载下一页数据。这个会在RemoteMediator
里面会使用得到。- 通过调用
collectAsGenerationalViewportHints
实现了PREPEND
和APPEND
两个操作。关于consumePrependGenerationIdAsFlow
和consumeAppendGenerationIdAsFlow
方法,我们这里就不讲解了,因为这个涉及到maxSize
和DropEvent,所涉及的内容就非常多,这里就不展开了。
我们这里直接使用来看collectAsGenerationalViewportHints
方法:
private suspend fun Flow<Int>.collectAsGenerationalViewportHints( loadType: LoadType ) = flatMapLatest { generationId ->
// Reset state to Idle and setup a new flow for consuming incoming load hints.
// Subsequent generationIds are normally triggered by cancellation.
stateLock.withLock {
// Skip this generationId of loads if there is no more to load in this
// direction. In the case of the terminal page getting dropped, a new
// generationId will be sent after load state is updated to Idle.
if (state.sourceLoadStates.get(loadType) == NotLoading.Complete) {
return@flatMapLatest flowOf()
} else if (state.sourceLoadStates.get(loadType) !is Error) {
state.setSourceLoadState(loadType, NotLoading.Incomplete)
}
}
@OptIn(FlowPreview::class)
hintChannel.asFlow()
// Prevent infinite loop when competing PREPEND / APPEND cancel each other
.drop(if (generationId == 0) 0 else 1)
.map { hint -> GenerationalViewportHint(generationId, hint) }
}
// Prioritize new hints that would load the maximum number of items.
.runningReduce { previous, next ->
if (next.shouldPrioritizeOver(previous, loadType)) next else previous
}
.conflate()
.collect { generationalHint ->
doLoad(state, loadType, generationalHint)
}
关于collectAsGenerationalViewportHints
方法,内部做了如下几件事:
- 通过
flatMapLatest
操作流拍平。因为hintChannel.asFlow()
方法返回的一个Flow,所以需要拍平。其次,我们这里需要注意一下,如果generationId
为0,表示当前没有进行Drop的操作,那么就不跳过第一个;如果不为0,那么进行了Drop操作,那么就跳过第一个,因为在进行Drop操作时,这里会收到一个generationId
,关于这个点,待会我单独讲解,这里先不展开。- 通过
conflate
操作符跳过之前发送的事件,保证只会取到一个事件。这个类似于RxJava里面的被压问题,有兴趣的同学可以看看这个操作符的原理,这里就不讲解了。- 最后,就是调用
doLoad
方法数据请求。
说了这么多,抛开中间很多没用的信息,其实从调用UiReceiver
的accessHint
方法开始,最终会调用到doLoad
方法进行网络请求。
好了接下来,我们将重点分析doLoad
方法,此方法涉及的内容非常的多,大家要有心里准备,不过我会尽最大的努力给大家解释清楚。
private suspend fun doLoad( state: PageFetcherSnapshotState<Key, Value>, loadType: LoadType, generationalHint: GenerationalViewportHint ) {
// 1. 计算已经加载的数量。
var itemsLoaded = 0
stateLock.withLock {
when (loadType) {
PREPEND -> {
val firstPageIndex =
state.initialPageIndex + generationalHint.hint.originalPageOffsetFirst - 1
for (pageIndex in 0..firstPageIndex) {
itemsLoaded += state.pages[pageIndex].data.size
}
}
APPEND -> {
val lastPageIndex =
state.initialPageIndex + generationalHint.hint.originalPageOffsetLast + 1
for (pageIndex in lastPageIndex..state.pages.lastIndex) {
itemsLoaded += state.pages[pageIndex].data.size
}
}
REFRESH -> throw IllegalStateException("Use doInitialLoad for LoadType == REFRESH")
}
}
// 2. 通过已经加载的数量获取key。
var loadKey: Key? = stateLock.withLock {
state.nextLoadKeyOrNull(loadType, generationalHint, itemsLoaded)
?.also { state.setLoading(loadType) }
}
var endOfPaginationReached = false
loop@ while (loadKey != null) {
val params = loadParams(loadType, loadKey)
// 3. 加载数据
val result: LoadResult<Key, Value> = pagingSource.load(params)
when (result) {
is Page<Key, Value> -> {
// ······
// 4. 插入数据
val insertApplied = stateLock.withLock {
state.insert(generationalHint.generationId, loadType, result)
}
// Break if insert was skipped due to cancellation
if (!insertApplied) break@loop
itemsLoaded += result.data.size
// 5. 如果nextKey为空,将endOfPaginationReached设置为true,
// 表示当前LoadType已经没有数据了。
if ((loadType == PREPEND && result.prevKey == null) ||
(loadType == APPEND && result.nextKey == null)
) {
endOfPaginationReached = true
}
}
// 省略Error的代码。
}
val dropType = when (loadType) {
PREPEND -> APPEND
else -> PREPEND
}
// 6. 进行Drop操作
stateLock.withLock {
state.dropEventOrNull(dropType, generationalHint.hint)?.let { event ->
state.drop(event)
pageEventCh.send(event)
}
loadKey = state.nextLoadKeyOrNull(loadType, generationalHint, itemsLoaded)
// Update load state to success if this is the final load result for this
// load hint, and only if we didn't error out.
if (loadKey == null && state.sourceLoadStates.get(loadType) !is Error) {
state.setSourceLoadState(
type = loadType,
newState = when {
endOfPaginationReached -> NotLoading.Complete
else -> NotLoading.Incomplete
}
)
}
// Send page event for successful insert, now that PagerState has been updated.
val pageEvent = with(state) {
result.toPageEvent(loadType)
}
// 7. 发送事件到UI层。
pageEventCh.send(pageEvent)
}
// 省略RemoteMediator的代码。
}
}
通过上面的代码,以及我添加的注释,我们可以知道,doLoad
方法一共做了4件事:
- 计算已经加载数据的数量,然后获取对应的key,用以后面的数据请求。在这里,就用到了之前在创建
ViewportHint
所携带的信息。- 调用
PagingSource
的load方法,进行数据请求。如果请求成功,会通过PageFetcherSnapshotState
的insert
方法把对应的数据插入进去,这个操作我们在Refresh里面看到过了,这里就不讲解了。- 进行Drop操作,尝试丢弃失效的页面。这一步非常的重要,这个对于我们理解前面所说的
startConsumingHints
有很大的帮助。这里我先不讲解它,后续会重点分析它。- 通过
pageEventCh
发送一个PageEvent
用来告知UI层,数据已经在加载完成。这个我们在前面分析过了,这里也不再讲解了。
总的来说,doLoad
方法的实现还是比较简单的,当然这里省略很多的细节,比如说Drop操作。不过,整个流程我们理解还是比较清晰,这里我对加载更多做一个简单的总结,方便大家来理解。
加载更多是一个UI层主动,数据请求层被动的过程。UI层通过调用
UiReceiver
的accessHint
方法来告知数据请求层需要进行加载更多的数据请求,在调用的同时UI层通过传递一个ViewportHint
对象用来携带一些关键信息。数据请求层监听到这个行为,并且拿到ViewportHint
对象,通过一系列的计算获取一个key,进而调用PagingSource
的load
方法进行数据请求,数据请求完成之后,进行了一些常规操作之后,通过pageEventCh
发送一个PageEvent
用来告知UI层,数据已经在加载完成。
如上便是加载更多的整个过程。接下来,为了大家理解更加的深刻,我将对drop操作进行分析,算是额外的福利内容,因为内容大纲并没有写这个。
6. Drop操作
前面在分析加载更多的时候,反复的提到Drop操作,包括在介绍PageEvent时,也介绍Drop事件。那么到底什么是Drop,什么时候进行Drop操作呢?
一言以蔽之,Drop可以理解为裁剪,当我们在创建PagingConfig
时,有一个配置项–maxSize
,表示当前数据总量。需要特别注意的是,这个maxSize
表示的意思并不是数据最大的数量,而是Adapter内部的List可以存储有效数据(非空数据)的最大数据量。比如说,我们将maxSize 设置为200,那么表示Adapter内部存储只能200条,超过200条之后从头开始丢弃。回到PagePresenter
里面来,这个类里面有三个变量用来统计数据总量,但是统计的数据是不同的,如下:
名字 | 含义 |
---|---|
storageCount | 真实的数据总量,不包括为空的数据量。 |
placeholdersBefore | 前置占位符的数量,这个范围里面的数据获取都为空。 |
placeholdersAfter | 后置占位符的数量。 |
Adapter 在统计数据总量(getItemCount)时,是通过PagePresenter
的size
方法来获取的。即上述三个总量的和:
override val size: Int
get() = placeholdersBefore + storageCount + placeholdersAfter
而PagingConfig
里面的maxSize
限制的是storageCount
,这一点大家一定要清楚。
其次,maxSize
只在enablePlaceholders
为true生效,切记切记!!
回到doLoad
方法,它之所以要在数据请求之后,且插入之后,进行裁剪操作,是为了让maxSize 这个配置项生效,不能让总数据量超过设置的阈值。那么它是在怎么进行裁剪的呢?主要分为两步(下面代码是doLoad方法部分代码片段):
// 1. 通过dropEventOrNull方法计算需要裁剪的数据
state.dropEventOrNull(dropType, generationalHint.hint)?.let { event ->
// 2.裁剪数据
state.drop(event)
pageEventCh.send(event)
}
这部分所做内容的如下:
- 调用
dropEventOrNull
方法计算需要裁剪的数据,如果需要裁剪,那么会返回一个Drop
的PageEvent;如果不需要裁剪,那么就会返回为空。- 通过DropEvent裁剪数据。首先是调用了
PageFetcherSnapshotState
的drop方法,将内部存储对应的数据删除;其次就发送一个PageEvent到UI层,告知UI层也要对应的删除。
我们来看看PageFetcherSnapshotState
的drop方法的实现:
fun drop(event: PageEvent.Drop<Value>) {
// .....
when (event.loadType) {
// 省略PREPEND的代码
APPEND -> {
repeat(event.pageCount) { _pages.removeAt(pages.size - 1) }
placeholdersAfter = event.placeholdersRemaining
appendLoadId++
appendLoadIdCh.offer(appendLoadId)
}
// ......
}
}
这里我们只看APPEND
操作,这个方法做了两件事,两件事都非常重要:
- 移除
_pages
里面对应的数据。- 将appendLoadId++,并且通过
appendLoadIdCh
发送出去。
那么哪里在消费appendLoadId
事件呢?这就要回到加载更多的地方,当时提到了consumeAppendGenerationIdAsFlow
方法。是的,就是这个方法对外提供消费入口:
@OptIn(ExperimentalCoroutinesApi::class)
fun consumeAppendGenerationIdAsFlow(): Flow<Int> {
return appendLoadIdCh.consumeAsFlow()
.onStart { appendLoadIdCh.offer(appendLoadId) }
}
从这里,我们便知道前面在collectAsGenerationalViewportHints
方法里面,为啥在generationId
(即generationId
)不为0时,需要丢弃一个数据了。不为0表示在进行Drop,如果PREPEND
和APPEND
同时进行加载,并且同时Drop,可能会导致死循环,所以需要跳过一个,让任意一个加载成功,另一个加载失败(因为会Drop)。
如上便是Drop的所有内容。
7. 总结
到此,上篇的内容到此结束,在这里,我对本文内容做一个简单的总结。
- Paging3相比于Paging2,PagingSource(即Paging2的DataSource)Api简洁了许多,使用起来也方便多了。
- 整个Paing3可以分为两层,分别是:数据请求层和Ui层。两个层之间通过Flow连接起来的。
- Refresh对于数据请求层来说,是一个主动的过程,主要是通过
PageFetcherSnapshot
的doInitialLoad
方法进行请求的。数据请求的基本过程如下:请求前,更新对应LoadType的LoadState,并且同步到Ui层;其次,通过调用PagingSource
的load
方法获取一个Load.Result对象;然后,根据Load.Result的类型进行不同的操作,如果是Load.Page对象,主要是过程是,更新对应LoadType的LoadState,将数据添加到PageFetcherSnapshotState
里面,同时发送一个PageEvent到Ui层。- Append对于数据请求层来说是一个被动的过程,由UI层触发。主要是
UiReceiver
作为桥梁进行请求,最终会调用PageFetcherSnapshot
的doLoad
方法。请求的过程跟Refresh类似,只不过这个过程多了Drop操作,Drop
主要是跟PagingConfig
里面的maxSize
有关。
下篇我将分析RemoteMediator
,敬请期待。
今天的文章Jetpack 源码分析(五) – Paging3源码分析(上)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/19010.html