??本篇是Paging3源碼分析的下篇拗踢,將重點(diǎn)介紹RemoteMediator
的實(shí)現(xiàn)原理。網(wǎng)絡(luò)上有很多的文章介紹這個(gè)多級數(shù)據(jù)源工具類,但是多多少少有點(diǎn)問題曲管,一般都沒有徹底理解清楚RemoteMediator
整個(gè)過請求的流程户侥。本文將從源碼角度解析RemoteMediator
的實(shí)現(xiàn)原理镀琉,同時(shí)也會分享RemoteMediator
的一些小建議。
??本文內(nèi)容續(xù)接上篇內(nèi)容蕊唐,建議先看一下上篇文章:Jetpack 源碼分析(五) - Paging3源碼分析(上)屋摔。
??本文主要內(nèi)容如下:
- 多級數(shù)據(jù)源的請求過程。
- 分別分析
RemoteMediator
和PagingSource
的實(shí)現(xiàn)細(xì)節(jié)替梨。- Refresh操作钓试,Prepend操作,Append操作在多級數(shù)據(jù)源和單一數(shù)據(jù)源中的不同副瀑。
- 關(guān)于
RemoteMediator
使用的一些小建議弓熏。
??本文參考資料:
??注意,本文Paging源碼均來自于3.0.0-alpha08版本糠睡。
1. RemoteMediator的請求過程
??相比于單一數(shù)據(jù)源挽鞠,RemoteMediator
多了一個(gè)過程--從網(wǎng)絡(luò)上獲取數(shù)據(jù)放到數(shù)據(jù)庫中。那么在多級數(shù)據(jù)源中,怎么將數(shù)據(jù)庫中的數(shù)據(jù)拿到UI層去顯示呢信认?這個(gè)就要說到PagingSource
串稀。
??在這之前,我先對PaingSource
和RemoteMediator
做一個(gè)解釋狮杨,方便大家理解母截,因?yàn)樗鼈z的工作是不一樣的。
PaingSource
:用于獲取UI層需要的數(shù)據(jù)橄教,且只能從一個(gè)地方獲取清寇,這也就是所謂的單一數(shù)據(jù)源
。UI層需要的數(shù)據(jù)都是通過該類來獲取的护蝶,包括在多級數(shù)據(jù)源里面华烟,PaingSource
負(fù)責(zé)從數(shù)據(jù)庫里面獲取數(shù)據(jù)。RemoteMediator
:主要的作用也是獲取數(shù)據(jù)持灰,只是它是從網(wǎng)絡(luò)上(或者其他地方)獲取數(shù)據(jù)盔夜,然后放到本地?cái)?shù)據(jù)庫,供PaingSource
從本地?cái)?shù)據(jù)庫中獲取數(shù)據(jù)堤魁。需要特別注意的是喂链,RemoteMediator
的數(shù)據(jù)不會直接用于UI顯示,而是保存在數(shù)據(jù)庫中妥泉。
??同時(shí)椭微,我畫了一張圖來幫助大家來理解這兩個(gè)類是如何配合工作的(官網(wǎng)的流程圖很容易誤解,以為RemoteMediator
獲取的數(shù)據(jù)也會用于Ui層顯示)盲链。
??整個(gè)請求過程如下:
首先蝇率,當(dāng)是第一次請求時(shí),
RemoteMediator
會進(jìn)行一次刷新操作刽沾,此時(shí)會請求到第一批數(shù)據(jù)本慕,同時(shí)會將這這批數(shù)據(jù)放到本地?cái)?shù)據(jù)庫里面,此時(shí)對應(yīng)的PagingSource對應(yīng)的從數(shù)據(jù)庫加載數(shù)據(jù)(需要注意的是侧漓,RemoteMediator
和PagingSource
是搭配使用的)锅尘。PagingSource的請求過程跟普通的請求類似,這個(gè)我們已經(jīng)在上篇文章介紹過了火架,有興趣的同學(xué)可以看看:Jetpack 源碼分析(五) - Paging3源碼分析(上)鉴象。最終會通過發(fā)送PageEvent將PagingSource獲取的數(shù)據(jù)傳遞給Ui層忙菠。
不過在這個(gè)過程中何鸡,我們有兩個(gè)問題:
- 當(dāng)PagingSource發(fā)現(xiàn)數(shù)據(jù)不夠了,怎么通知
RemoteMediator
繼續(xù)請求牛欢。- 除了第一個(gè)頁面數(shù)據(jù)的加載骡男,
RemoteMediator
是怎么加載其他頁面的數(shù)據(jù),以及加載完成之后怎么通知PagingSource獲取的呢傍睹?
對于這兩個(gè)問題隔盛,這里就不展開分析犹菱。下面我們將重點(diǎn)分析這兩個(gè)問題和其他很多的問題,比如吮炕,之前介紹PagPresenter
的時(shí)候腊脱,說它內(nèi)部存儲了所有的數(shù)據(jù),在RemoteMediator
中就不成立龙亲;以及陕凹,即使不手動(dòng)刷新,PagingSource也會進(jìn)行Refresh操作鳄炉。
??那么多級數(shù)據(jù)源是怎么從數(shù)據(jù)庫里面獲取的數(shù)據(jù)呢杜耙?我們在ViewModel內(nèi)部定義Flow時(shí),直接從Dao里面獲取一個(gè)PagingSource 對象拂盯,并不是我們自己定義的佑女。這個(gè)PagingSource
實(shí)際上是一個(gè)LegacyPagingSource
,是Paging3框架內(nèi)部的一個(gè) 實(shí)現(xiàn)谈竿。從數(shù)據(jù)庫獲取數(shù)據(jù)的整個(gè)過程团驱,主要是通過LegacyPagingSource
的load方法,調(diào)到了LimitOffsetDataSource
里面去了(注意這里是DataSource空凸,對的店茶,就是Paging2里面的DataSource),LimitOffsetDataSource
是PositionalDataSource
的子類劫恒,內(nèi)部處理了從數(shù)據(jù)庫獲取數(shù)據(jù)的操作贩幻。也就是說,LegacyPagingSource
其實(shí)只是一個(gè)Wrapper两嘴,用來抹平Paging2和Paging3之間的差異丛楚。
2. RemoteMediator的觸發(fā)
??我們知道RemoteMediator
請求是通過load方法進(jìn)行,那么哪里在調(diào)用這個(gè)方法呢憔辫?在PageFetcher
和PageFetcherSnapshot
內(nèi)部并沒有直接調(diào)用調(diào)用RemoteMediator
的方法趣些,而是通過RemoteMediatorAccessor
來輔助調(diào)用的。RemoteMediatorAccessor
內(nèi)部封裝了很多RemoteMediator
的調(diào)用邏輯贰您,包括首次加載和加載更多坏平,主要是通過內(nèi)部的launchRefresh
和launchBoundary
完成對RemoteMediator
的調(diào)用。同時(shí)關(guān)于RemoteMediatorAccessor
锦亦,我們還需要注意一點(diǎn)舶替,該對象只會在首次刷新創(chuàng)建一次,這一點(diǎn)跟PageFetcherSnapshot
有很大的不同杠园,之所以要這樣做顾瞪,是因?yàn)?code>RemoteMediatorAccessor有很多全局的狀態(tài),不能因?yàn)镽efresh而丟失了。
??RemoteMediator
的觸發(fā)請求主要分為兩種:Refresh和Append(Prepend)陈醒。我們分開來看一下他們的細(xì)節(jié)惕橙。
(1). Refresh
??在PageFetcherSnapshot
內(nèi)部有一個(gè)Flow對象--pageEventFlow
,這個(gè)對象初始化的時(shí)候钉跷,定義幾段代碼弥鹦,用來實(shí)現(xiàn)RemoteMediator
觸發(fā)Refresh請求,主要代碼如下:
if (triggerRemoteRefresh) {
remoteMediatorConnection?.let {
val pagingState = stateLock.withLock { state.currentPagingState(null) }
it.requestLoad(REFRESH, pagingState)
}
}
??這段代碼非常的簡單爷辙,但是內(nèi)部蘊(yùn)含的信息可不少惶凝,主要有三點(diǎn):
- 首先,判斷是否
triggerRemoteRefresh
是否為true犬钢,為true進(jìn)行Refresh操作苍鲜。為啥要判斷這個(gè)變量呢?因?yàn)檫@段代碼會調(diào)用的話,表示在進(jìn)行Refresh操作玷犹,但是不代表RemoteMediator
必須要刷新數(shù)據(jù)(RemoteMediator
刷新數(shù)據(jù)時(shí)混滔,需要將數(shù)據(jù)庫中舊數(shù)據(jù)清除掉。)歹颓。在單一數(shù)據(jù)源中坯屿,只要不手動(dòng)Refresh,可能永遠(yuǎn)不會有第二次Refrsh操作進(jìn)行(這里只是說的可能巍扛,因?yàn)椴荒鼙WC100%领跛,PagingConfig
里面的jumpThreshold
字段會打破這個(gè)規(guī)則),但是在RemoteMediator
中撤奸,如果本地?cái)?shù)據(jù)庫中的數(shù)據(jù)不夠了吠昭,PagingSource可能會觸發(fā)多次Refresh(正常滑動(dòng)觸發(fā)的)胧瓜,所以上述的代碼可能會調(diào)用多次矢棚。因此需要通過triggerRemoteRefresh
來過濾條件。同時(shí)從另一個(gè)方面來看府喳,其他地方可以手動(dòng)的調(diào)用PagingSource的invalidate
和Adapter的refresh
方法來觸發(fā)刷新蒲肋,那么這兩個(gè)方法有啥區(qū)別:
?(1).invalidate
只是表示當(dāng)前PagingSource失效了,會重新創(chuàng)建的創(chuàng)建一個(gè)新的PagingSource钝满,這個(gè)過程不會影響原來的已有的數(shù)據(jù)兜粘。這個(gè)方法一般不允許外部手動(dòng)調(diào)用。
?(2).refresh
表示需要所有的數(shù)據(jù)清空弯蚜,重新進(jìn)行請求孔轴。比如說,我們進(jìn)行了下拉刷新熟吏,此時(shí)就會調(diào)用這個(gè)方法距糖。
同時(shí),我們從兩個(gè)方法實(shí)現(xiàn)也能看出來區(qū)別牵寺,refresh
給refreshChannel
傳的是true悍引,即triggerRemoteRefresh
為true;invalidate
方法傳的是false帽氓,即triggerRemoteRefresh
為false趣斤。為了理解清晰,介紹簡單黎休,我將refresh
觸發(fā)的刷新稱之為完全刷新浓领,invalidate
觸發(fā)的刷新稱之為不完全刷新,下述內(nèi)容統(tǒng)一用這個(gè)來表示势腮。- 將
PageFetcherSnapshotState
內(nèi)部的PagingState
設(shè)置為null联贩,這一步主要是為了輔助完全刷新。在Paging刷新過程中捎拯,會獲取Refresh key泪幌,用來判斷加載哪部分的數(shù)據(jù);如果這個(gè)key為空署照,表示是完全刷新祸泪,如果是不為空,那么表示是不完全刷新建芙,這部分的代碼在LegacyPagingSource
的getRefreshKey
方法里面没隘,有興趣的同學(xué)可以看看。- 調(diào)用
RemoteMediatorConnection
的requestLoad
方法禁荸,進(jìn)行刷新的數(shù)據(jù)請求右蒲。requestLoad
方法非常的重要,因?yàn)?code>RemoteMediator在觸發(fā)網(wǎng)絡(luò)請求時(shí)赶熟,都是通過這個(gè)方法實(shí)現(xiàn)的品嚣。
??接下來,我們來分析一下requestLoad
方法钧大,直接來看代碼:
override fun requestLoad(loadType: LoadType, pagingState: PagingState<Key, Value>) {
// 1. 往任務(wù)隊(duì)列中添加一個(gè)任務(wù)翰撑。
val newRequest = accessorState.use {
it.add(loadType, pagingState)
}
// 進(jìn)行網(wǎng)絡(luò)請求。
if (newRequest) {
when (loadType) {
LoadType.REFRESH -> launchRefresh()
else -> launchBoundary()
}
}
}
??requestLoad
方法內(nèi)部主要是做了兩件事:
- 通過add方法往任務(wù)隊(duì)列里面添加一個(gè)任務(wù)啊央。在
RemoteMediatorAccessImpl
內(nèi)部眶诈,維護(hù)了一個(gè)pendingRequests
隊(duì)列,里面存儲著三種LoadType的任務(wù)瓜饥。在添加的時(shí)候主要是check兩件事:首先判斷當(dāng)前任務(wù)隊(duì)列中是否已經(jīng)有對應(yīng)LoadType的任務(wù)逝撬;其次,當(dāng)前任務(wù)是否處于未鎖定的狀態(tài)乓土。只有這兩個(gè)條件同時(shí)滿足宪潮,才能添加成功溯警,也才能進(jìn)行第二步操作。- 調(diào)用
launchRefresh
方法進(jìn)行網(wǎng)絡(luò)請求狡相。
??我們來看一下launchRefresh
:
private fun launchRefresh() {
scope.launch {
var launchAppendPrepend = false
isolationRunner.runInIsolation(
priority = PRIORITY_REFRESH
) {
val pendingPagingState = accessorState.use {
it.getPendingRefresh()
}
pendingPagingState?.let {
// 調(diào)用RemoteMediator的load方法梯轻,進(jìn)行網(wǎng)絡(luò)請求。
val loadResult = remoteMediator.load(LoadType.REFRESH, pendingPagingState)
launchAppendPrepend = when (loadResult) {
is MediatorResult.Success -> {
// 更新狀態(tài)尽棕,并且從隊(duì)列中移除相關(guān)任務(wù)喳挑。
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 -> {
// 如果請求失敗,那么看看隊(duì)列中是否Append或者Prepend的任務(wù)滔悉,如果有的話伊诵,那么就
// 執(zhí)行。
accessorState.use {
it.clearPendingRequest(LoadType.REFRESH)
it.setError(LoadType.REFRESH, LoadState.Error(loadResult.throwable))
it.getPendingBoundary() != null
}
}
}
}
}
if (launchAppendPrepend) {
launchBoundary()
}
}
}
??在launchRefresh
方法里面主要是做了兩件事:
- 調(diào)用
RemoteMediator
的load方法回官。因?yàn)閘oad方法是自己定義曹宴,所以做了啥事,我們都很清楚歉提,這里就不展開了浙炼。- 其次,就是更新對應(yīng)的狀態(tài)唯袄。如果請求成功的話弯屈,那么會把
APPEND
和PREPEND
釋放,保證后面可以正常進(jìn)行操作恋拷;如果是請求失敗的話资厉,除了更新狀態(tài)之外,還通過調(diào)用getPendingBoundary
判斷當(dāng)前任務(wù)隊(duì)列是否有Append
和Prepend
的任務(wù)蔬顾,如果有的話宴偿,就會調(diào)用進(jìn)行請求,即調(diào)用launchBoundary
诀豁。
(2). Append
??為了簡單起見窄刘,這里只看Append的場景,Prepend跟Append比較類似舷胜,這里就不贅述了娩践。
??Refresh的觸發(fā)過程,我們已經(jīng)理解了烹骨,接下來我們來看一下Append的觸發(fā)過程翻伺。Append是怎么觸發(fā)的呢?用一句話來總結(jié)沮焕,就是當(dāng)PagingSource加載完成數(shù)據(jù)后吨岭,根據(jù)請求回來的數(shù)據(jù)(包括PagingSource的Refresh和Append兩種方式)來判斷是否需要觸發(fā)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關(guān)于是否進(jìn)行RemoteMediator
的Append操作的判斷條件非常相似峦树,就是看請求回來的數(shù)據(jù)的nextKey(prevKey)是否為空辣辫,如果為空就表示需要進(jìn)行RemoteMediator
的Append操作旦事,即需要從網(wǎng)絡(luò)拉取新的數(shù)據(jù)。那么nextKey
為空表示的是什么意思呢急灭?
總的來說姐浮,
nextKey
為空就表示當(dāng)前數(shù)據(jù)已經(jīng)加載到數(shù)據(jù)庫種的已有數(shù)據(jù)的邊界,此時(shí)就必須要從網(wǎng)絡(luò)網(wǎng)絡(luò)上加載下一頁數(shù)據(jù)了化戳,否則的話单料,用戶馬上就要滑不動(dòng)了埋凯。那么為啥nextKey
就表示已經(jīng)到了數(shù)據(jù)邊界呢点楼?在Paging2里面,數(shù)據(jù)請求有一個(gè)概念白对,就是totalCount
掠廓,如果最后一個(gè)數(shù)據(jù)項(xiàng)的位置等于這個(gè)totalCount
,那么表示已經(jīng)到了邊界,此時(shí)nextKey
就會為空甩恼。
這里的nextKey
涉及到了PagingSource的加載過程蟀瞧,以及LegacyPagingSource
和LimitOffsetDataSource
的加載,我們先不展開分析条摸,后續(xù)有內(nèi)容會重點(diǎn)分析這個(gè)悦污。
??Append的觸發(fā)最終也調(diào)用到了RemoteMediatorConnection
的requestLoad
方法里面,我們之前已經(jīng)看過這個(gè)方法了钉蒲,這里直接看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
做的事非常的簡單切端,就是調(diào)用RemoteMediator
的load
方法,請求下一批數(shù)據(jù)到數(shù)據(jù)庫中顷啼。操作跟Refresh基本類似踏枣,這里就不再分析了。
??到此钙蒙,關(guān)于RemoteMediator
觸發(fā)過程的內(nèi)容就結(jié)束了茵瀑,在這里,我們對此做一個(gè)小小的總結(jié)躬厌,以便大家腦海中有一個(gè)印象马昨。
Paging3內(nèi)部的刷新可以分為兩種:完全刷新(調(diào)用
PageFetcher
的refresh
方法)和不完全刷新(調(diào)用PageFetcher
的invalidate
方法)。這兩種刷新不同點(diǎn)在于扛施,完全刷新時(shí)偏陪,RemoteMediator
會清空已有的數(shù)據(jù),重新請求數(shù)據(jù)煮嫌;而不完全刷新則不會笛谦。這個(gè)主要通過PageFetcherSnapshot
的triggerRemoteRefresh
來控制的。RemoteMediator
的刷新主要是通過RemoteMediatorConnection
的requestLoad方法觸發(fā)昌阿,其方法內(nèi)部調(diào)用launchRefresh
方法饥脑,進(jìn)而調(diào)用了RemoteMediator
的load方法恳邀。需要特別注意的是,requestLoad
方法是外部(PageFetcherSnapshot)觸發(fā)RemoteMediator
加載網(wǎng)絡(luò)數(shù)據(jù)唯一途徑灶轰。
RemoteMediator
加載更多的觸發(fā)是在PagingSource請求完成之后才進(jìn)行的谣沸,當(dāng)發(fā)現(xiàn)已經(jīng)到了數(shù)據(jù)邊界,此時(shí)通過requestLoad
方法加載下一頁的數(shù)據(jù)笋颤。RemoteMediatorConnection
內(nèi)部通過launchBoundary
方法觸發(fā)RemoteMediator
的load方法乳附。關(guān)于數(shù)據(jù)邊界,主要是通過nextKey是為空來判斷的伴澄,這個(gè)涉及到PagingSource的加載過程赋除,我們馬上會分析。
3. PagingSource和DataSource的加載
??前面已經(jīng)說過了非凌,在多級數(shù)據(jù)源中举农,PagingSource是LegacyPagingSource
,DataSource是LimitOffsetDataSource
敞嗡。其中LegacyPagingSource
只起到了一個(gè)橋梁作用颁糟,保證在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方法的實(shí)現(xiàn)很簡單喉悴,最終調(diào)用了LimitOffsetDataSource
的load方法棱貌。不過這里有一點(diǎn)我們需要注意:
當(dāng)請求返回的數(shù)據(jù)為空,key會為空箕肃。數(shù)據(jù)為空婚脱,表示本地?cái)?shù)據(jù)庫沒有更多的數(shù)據(jù)可以加載,也就是說已經(jīng)加載到邊界了突雪,所以需要告訴
RemoteMediator
從網(wǎng)絡(luò)上請求更多的數(shù)據(jù)起惕。
關(guān)于這種情況,有一個(gè)問題:當(dāng)執(zhí)行PagingSource發(fā)現(xiàn)沒有更多的數(shù)據(jù)咏删,此時(shí)需要從網(wǎng)絡(luò)上獲取數(shù)據(jù)惹想,那么數(shù)據(jù)請求回來之后,怎么通知PagingSource來重新加載數(shù)據(jù)呢督函?這個(gè)就得說說Room的實(shí)現(xiàn)嘀粱,Room在初始化的時(shí)候,給我們的表創(chuàng)建了一個(gè)觸發(fā)器辰狡,用以監(jiān)聽表的更新锋叨,插入和刪除三種操作编矾。當(dāng)有新的數(shù)據(jù)更新到數(shù)據(jù)庫中去的時(shí)候扮授,觸發(fā)器會發(fā)送一個(gè)invalidate
的通知察署,這個(gè)通知會調(diào)用PagingSource的invalidate
方法葵擎,從而導(dǎo)致PagingSource重新創(chuàng)建和重新加載,此時(shí)就是所謂在上下滑動(dòng)過程也會觸發(fā)PagingSource的Refresh操作幕庐。關(guān)于觸發(fā)器的邏輯熔吗,有興趣的同學(xué)可以看看Room的一個(gè)類:InvalidationTracker
姆钉。但是按照正常邏輯來說,PagingSource重建之后听诸,Refresh獲取數(shù)據(jù)應(yīng)該是全新的坐求,怎么能保證數(shù)據(jù)前后銜接上呢?這是因?yàn)?code>initialKey的存在晌梨,因?yàn)樵趕can方法的時(shí)候會拿到創(chuàng)建之前的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
方法。這個(gè)方法在之前說不完全刷新時(shí)就提到了仔蝌,感興趣的同學(xué)可以看看(內(nèi)部主要通過PagingState
的anchorPosition
來計(jì)算key泛领,完全刷新時(shí),anchorPosition
要么為0掌逛,要么為空师逸,所以不會銜接之前的數(shù)據(jù))司倚。
??這里給大家補(bǔ)充了一下額外的知識豆混,我們繼續(xù)看一下DataSource的loadInitial
方法(load方法調(diào)用了loadInitial
方法,只是在load方法里面有一些計(jì)算动知,這些計(jì)算邏輯有興趣的同學(xué)可以自行看看皿伺,這里就不講解了):
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
方法里面主要做了兩件事:
- 調(diào)用另一個(gè)
loadInitial
方法獲取數(shù)據(jù)。這個(gè)loadInitial
方法就是從數(shù)據(jù)庫里面獲取數(shù)據(jù)盒粮,有興趣的同學(xué)可以看看鸵鸥,這里就不展開了。- 根據(jù)請求的結(jié)果丹皱,返回一個(gè)BaseResult妒穴。這里我們特別注意的是,當(dāng)
nextKey == totalCount
時(shí)摊崭,返回nextKey讼油,這個(gè)驗(yàn)證了我們之前的說法。
??至此PagingSource的Refresh加載就結(jié)束了呢簸,這里我省略Append的過程分析矮台,因?yàn)锳ppend過程和Refresh過程非常相似,只不過他們在調(diào)用的方法不一樣而已根时。Refresh調(diào)用的是loadInitial
方法瘦赫,Append 調(diào)用的loadRange
方法,其他地方都比較類似的蛤迎,這里就不過多的分析了确虱。
??在這里,我猜測大家心里面還有疑惑替裆,PagingSource的加載(Refresh 和Append)還是不理解校辩,這兩個(gè)操作是怎么關(guān)聯(lián)起來的呢唱较?接下來,我將繼續(xù)給大家解疑答惑召川。
4. PagingSource的Refresh和Append關(guān)聯(lián)
??在單一數(shù)據(jù)源中南缓,我們都知道PagingSource一次完整的加載過程包括:一次Refresh + 多次Append + 多次Prepend。但是在多級數(shù)據(jù)源中卻不是這樣的荧呐,在多級數(shù)據(jù)源中汉形,PagingSource完整加載過程是:[一次Refresh + 多次Append + 多次Prepend] + [一次Refresh + 多次Append + 多次Prepend]...... 。
注意倍阐,在多級數(shù)據(jù)源中概疆,
RemoteMediator
的完整加載過程是:一次Refresh + 多次Append + 多次Prepend。PagingSource的完整過程不是這樣的峰搪,這一點(diǎn)一定要明確岔冀。
??上面已經(jīng)簡單的說明了PagingSource完整加載過程,在這里我們詳細(xì)的解釋一下概耻。主要從兩個(gè)方面來說:
- Refresh + Append:當(dāng)Refresh一次之后使套,本地會預(yù)取一批(不只是一頁數(shù)據(jù),這里默認(rèn)為pagSize大小的數(shù)據(jù)量為一頁)的數(shù)據(jù)鞠柄,我們通過向下滑動(dòng)的操作會將預(yù)取的數(shù)據(jù)一頁一頁(即每次取pageSize大小的數(shù)據(jù))的Append到UI層侦高。當(dāng)這次Refresh的數(shù)量被消費(fèi)完畢,即滑動(dòng)到邊界(或者說厌杜,預(yù)取的數(shù)據(jù)已經(jīng)被完全加載到UI層了)奉呛,此時(shí)nextKey為會空,從而再次觸發(fā)
RemoteMediator
的網(wǎng)絡(luò)請求(此時(shí)RemoteMediator
的loadType是Append)夯尽。RemoteMediator
請求完成之后瞧壮,更新到數(shù)據(jù)庫中,觸發(fā)器會通知PagingSource重新創(chuàng)建并且Refresh(不完全刷新)匙握,再開始一輪的Append咆槽。同理Prepend也是類似的。- Prepend:首先肺孤,這里
Prepend
說的不是從數(shù)據(jù)庫獲取新的數(shù)據(jù)罗晕,而是獲取舊的數(shù)據(jù)。比如說赠堵,當(dāng)前小渊,我們向下滑動(dòng)到200位置,在向上滑動(dòng)茫叭,會進(jìn)行Prepend操作(即從數(shù)據(jù)庫獲取之前的數(shù)據(jù))酬屉。為啥會這樣呢?因?yàn)槎嗉墧?shù)據(jù)源中,PagePresenter不會保存所有的數(shù)據(jù)(即打破了保存所有數(shù)據(jù)的規(guī)則呐萨,前面已經(jīng)說過)杀饵,最多會保留initialLoadSize
大小的數(shù)據(jù)。其他的數(shù)據(jù)都會用占位符來代替谬擦,即PagePresenter
里面的placeholdersBefore
和placeholdersAfter
切距,當(dāng)再次需要使用的時(shí)候,會重新從數(shù)據(jù)庫中加載并且顯示惨远。
??上述第二點(diǎn)谜悟,我們從代碼找到答案,就拿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
)
)
}
}
??隨著我們不斷向下滑動(dòng)北秽,position會變得越來越大葡幸,因此itemsBefore
就會變得越來越大(即PagePresenter
的placeholdersBefore
)。但是贺氓,我們的有效數(shù)據(jù)總量不會變大蔚叨,始終是initialLoadSize
這么多。因此辙培,當(dāng)我們使用RemoteMediator
時(shí)蔑水,不要嘗試獲取任意位置的數(shù)據(jù),因?yàn)楂@取的有可能是空虏冻。
5. 使用RemoteMediator的一些小建議
??至此肤粱,源碼分析我們算是結(jié)束了弹囚,在這里厨相,我對使用RemoteMediator
提一些小建議。
(1). 不要隨意的調(diào)用Adapter的getItem方法獲取任意位置的數(shù)據(jù)
??因?yàn)?code>PagePresenter只會保留initialLoadSize
大小的有效數(shù)據(jù)鸥鹉,其他位置都會用null來填充蛮穿,所以通過getItem
方法獲取的數(shù)據(jù)很有可能為空,容易造成不必要的錯(cuò)誤毁渗。
(2). 盡量將PageConfig的initialLoadSize設(shè)置的大一點(diǎn)
??因?yàn)樵诨瑒?dòng)過程中践磅,PagingSource的Append和Prepend操作消費(fèi)的是RemoteMediator獲取的initialLoadSize
大小的數(shù)據(jù),將initialLoadSize
設(shè)置大一點(diǎn)灸异,可以減少RemoteMediator
的請求府适。其次最好將initialLoadSize
設(shè)置為pageSize
整數(shù)倍,避免在Append和Prepend時(shí)出現(xiàn)斷頁的情況肺樟。
(3). 最好RemoteMediator每次請求的數(shù)量量都設(shè)置成為一樣檐春,且都為initialLoadSize
??對于RemoteMediator
來說,每次請求雖然loadType不一樣么伯,但是本質(zhì)都是差不多的疟暖,都是PagingSource發(fā)現(xiàn)數(shù)據(jù)不夠了,需要新增數(shù)據(jù)。所以每次請求的數(shù)據(jù)都一樣俐巴,能保證邏輯簡單且統(tǒng)一骨望。參考代碼如下:
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存儲的數(shù)據(jù)不是所有的數(shù)據(jù)
??PagingState
內(nèi)部存儲的數(shù)據(jù)并不是所有的數(shù)據(jù),而是上一次Refresh的數(shù)據(jù)欣舵,不要嘗試通過這個(gè)變量來計(jì)算所有數(shù)據(jù)的總數(shù)擎鸠。不過,可以通過如下代碼計(jì)算缘圈,但是不能保證100%靠譜糠亩,因?yàn)?code>itemsBefore和itemsAfter
可能是無效值。
val pages = state.pages
var totalCount = 0
if(pages.isNotEmpty()){
pages.forEach {
totalCount += it.data.size + it.itemsBefore + it.itemsAfter
}
}
6. 總結(jié)
??到這里准验,Paging3源碼分析的內(nèi)容就結(jié)束了赎线,我做了一個(gè)簡單的總結(jié):
- 正常情況下,
RemoteMediator
只會Refresh一次糊饱,除非手動(dòng)Refresh垂寥;PagingSource可能會多次Refresh,除了第一個(gè)初始化Refresh之外另锋,當(dāng)RemoteMediator
從網(wǎng)絡(luò)上獲取滞项,放到數(shù)據(jù)庫時(shí),PagingSource也會Refresh夭坪。- 在Paging3里面文判,分為兩種刷新,分別是不完全刷新室梅,即調(diào)用
PageFetcher
的invalidate
方法戏仓,在這種情況下,本地?cái)?shù)據(jù)庫的數(shù)據(jù)不會清空亡鼠,只會新增數(shù)據(jù)赏殃;完全刷新,即調(diào)用PageFetcher
的refresh
方法间涵,此種刷新會清空本地?cái)?shù)據(jù)庫的數(shù)據(jù)仁热。- 多級數(shù)據(jù)源中的PagingSource完整加載過程是:[一次Refresh + 多次Append + 多次Prepend] + [一次Refresh + 多次Append + 多次Prepend]......,這個(gè)跟單一數(shù)據(jù)源不一樣勾哩。