Jetpack 源碼分析(六) - Paging3源碼分析(下)

??本篇是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)容如下:

  1. 多級數(shù)據(jù)源的請求過程。
  2. 分別分析RemoteMediatorPagingSource的實(shí)現(xiàn)細(xì)節(jié)替梨。
  3. Refresh操作钓试,Prepend操作,Append操作在多級數(shù)據(jù)源和單一數(shù)據(jù)源中的不同副瀑。
  4. 關(guān)于RemoteMediator使用的一些小建議弓熏。

??本文參考資料:

  1. Page from network and database
  2. 使用 Paging 3 實(shí)現(xiàn)分頁加載
  3. Android Jetpack組件之?dāng)?shù)據(jù)庫Room詳解(三)

??注意,本文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串稀。
??在這之前,我先對PaingSourceRemoteMediator做一個(gè)解釋狮杨,方便大家理解母截,因?yàn)樗鼈z的工作是不一樣的。

  1. PaingSource:用于獲取UI層需要的數(shù)據(jù)橄教,且只能從一個(gè)地方獲取清寇,這也就是所謂的單一數(shù)據(jù)源。UI層需要的數(shù)據(jù)都是通過該類來獲取的护蝶,包括在多級數(shù)據(jù)源里面华烟,PaingSource負(fù)責(zé)從數(shù)據(jù)庫里面獲取數(shù)據(jù)。
  2. 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ù)(需要注意的是侧漓,RemoteMediatorPagingSource是搭配使用的)锅尘。PagingSource的請求過程跟普通的請求類似,這個(gè)我們已經(jīng)在上篇文章介紹過了火架,有興趣的同學(xué)可以看看:Jetpack 源碼分析(五) - Paging3源碼分析(上)鉴象。最終會通過發(fā)送PageEvent將PagingSource獲取的數(shù)據(jù)傳遞給Ui層忙菠。
不過在這個(gè)過程中何鸡,我們有兩個(gè)問題:

  1. 當(dāng)PagingSource發(fā)現(xiàn)數(shù)據(jù)不夠了,怎么通知RemoteMediator繼續(xù)請求牛欢。
  2. 除了第一個(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),LimitOffsetDataSourcePositionalDataSource的子類劫恒,內(nèi)部處理了從數(shù)據(jù)庫獲取數(shù)據(jù)的操作贩幻。也就是說,LegacyPagingSource其實(shí)只是一個(gè)Wrapper两嘴,用來抹平Paging2和Paging3之間的差異丛楚。

2. RemoteMediator的觸發(fā)

??我們知道RemoteMediator請求是通過load方法進(jìn)行,那么哪里在調(diào)用這個(gè)方法呢憔辫?在PageFetcherPageFetcherSnapshot內(nèi)部并沒有直接調(diào)用調(diào)用RemoteMediator的方法趣些,而是通過RemoteMediatorAccessor來輔助調(diào)用的。RemoteMediatorAccessor內(nèi)部封裝了很多RemoteMediator的調(diào)用邏輯贰您,包括首次加載和加載更多坏平,主要是通過內(nèi)部的launchRefreshlaunchBoundary完成對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):

  1. 首先,判斷是否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ū)別牵寺,refreshrefreshChannel傳的是true悍引,即triggerRemoteRefresh為true;invalidate方法傳的是false帽氓,即triggerRemoteRefresh為false趣斤。為了理解清晰,介紹簡單黎休,我將refresh觸發(fā)的刷新稱之為完全刷新浓领,invalidate觸發(fā)的刷新稱之為不完全刷新,下述內(nèi)容統(tǒng)一用這個(gè)來表示势腮。
  2. PageFetcherSnapshotState內(nèi)部的PagingState設(shè)置為null联贩,這一步主要是為了輔助完全刷新。在Paging刷新過程中捎拯,會獲取Refresh key泪幌,用來判斷加載哪部分的數(shù)據(jù);如果這個(gè)key為空署照,表示是完全刷新祸泪,如果是不為空,那么表示是不完全刷新建芙,這部分的代碼在LegacyPagingSourcegetRefreshKey方法里面没隘,有興趣的同學(xué)可以看看。
  3. 調(diào)用RemoteMediatorConnectionrequestLoad方法禁荸,進(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)部主要是做了兩件事:

  1. 通過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)行第二步操作。
  2. 調(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方法里面主要是做了兩件事:

  1. 調(diào)用RemoteMediator的load方法回官。因?yàn)閘oad方法是自己定義曹宴,所以做了啥事,我們都很清楚歉提,這里就不展開了浙炼。
  2. 其次,就是更新對應(yīng)的狀態(tài)唯袄。如果請求成功的話弯屈,那么會把APPENDPREPEND釋放,保證后面可以正常進(jìn)行操作恋拷;如果是請求失敗的話资厉,除了更新狀態(tài)之外,還通過調(diào)用getPendingBoundary判斷當(dāng)前任務(wù)隊(duì)列是否有AppendPrepend的任務(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的加載過程蟀瞧,以及LegacyPagingSourceLimitOffsetDataSource的加載,我們先不展開分析条摸,后續(xù)有內(nèi)容會重點(diǎn)分析這個(gè)悦污。

??Append的觸發(fā)最終也調(diào)用到了RemoteMediatorConnectionrequestLoad方法里面,我們之前已經(jīng)看過這個(gè)方法了钉蒲,這里直接看RemoteMediatorAccessorlaunchBoundary方法:

    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)用RemoteMediatorload方法,請求下一批數(shù)據(jù)到數(shù)據(jù)庫中顷啼。操作跟Refresh基本類似踏枣,這里就不再分析了。

??到此钙蒙,關(guān)于RemoteMediator觸發(fā)過程的內(nèi)容就結(jié)束了茵瀑,在這里,我們對此做一個(gè)小小的總結(jié)躬厌,以便大家腦海中有一個(gè)印象马昨。

Paging3內(nèi)部的刷新可以分為兩種:完全刷新(調(diào)用PageFetcherrefresh方法)和不完全刷新(調(diào)用PageFetcherinvalidate方法)。這兩種刷新不同點(diǎn)在于扛施,完全刷新時(shí)偏陪,RemoteMediator會清空已有的數(shù)據(jù),重新請求數(shù)據(jù)煮嫌;而不完全刷新則不會笛谦。這個(gè)主要通過PageFetcherSnapshottriggerRemoteRefresh來控制的。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。
??我們直接來看LegacyPagingSourceload方法:

    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)部主要通過PagingStateanchorPosition來計(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方法里面主要做了兩件事:

  1. 調(diào)用另一個(gè)loadInitial方法獲取數(shù)據(jù)。這個(gè)loadInitial方法就是從數(shù)據(jù)庫里面獲取數(shù)據(jù)盒粮,有興趣的同學(xué)可以看看鸵鸥,這里就不展開了。
  2. 根據(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è)方面來說:

  1. 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也是類似的。
  2. 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里面的placeholdersBeforeplaceholdersAfter切距,當(dāng)再次需要使用的時(shí)候,會重新從數(shù)據(jù)庫中加載并且顯示惨远。

??上述第二點(diǎn)谜悟,我們從代碼找到答案,就拿PositionalDataSourceloadInitial方法來說:

                    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就會變得越來越大(即PagePresenterplaceholdersBefore)。但是贺氓,我們的有效數(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é):

  1. 正常情況下,RemoteMediator只會Refresh一次糊饱,除非手動(dòng)Refresh垂寥;PagingSource可能會多次Refresh,除了第一個(gè)初始化Refresh之外另锋,當(dāng)RemoteMediator從網(wǎng)絡(luò)上獲取滞项,放到數(shù)據(jù)庫時(shí),PagingSource也會Refresh夭坪。
  2. 在Paging3里面文判,分為兩種刷新,分別是不完全刷新室梅,即調(diào)用PageFetcherinvalidate方法戏仓,在這種情況下,本地?cái)?shù)據(jù)庫的數(shù)據(jù)不會清空亡鼠,只會新增數(shù)據(jù)赏殃;完全刷新,即調(diào)用PageFetcherrefresh方法间涵,此種刷新會清空本地?cái)?shù)據(jù)庫的數(shù)據(jù)仁热。
  3. 多級數(shù)據(jù)源中的PagingSource完整加載過程是:[一次Refresh + 多次Append + 多次Prepend] + [一次Refresh + 多次Append + 多次Prepend]......,這個(gè)跟單一數(shù)據(jù)源不一樣勾哩。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抗蠢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子思劳,更是在濱河造成了極大的恐慌迅矛,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敢艰,死亡現(xiàn)場離奇詭異诬乞,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門震嫉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來森瘪,“玉大人,你說我怎么就攤上這事票堵《蟛牵” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵悴势,是天一觀的道長窗宇。 經(jīng)常有香客問我,道長特纤,這世上最難降的妖魔是什么军俊? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮捧存,結(jié)果婚禮上粪躬,老公的妹妹穿的比我還像新娘。我一直安慰自己昔穴,他們只是感情好镰官,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吗货,像睡著了一般泳唠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宙搬,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天笨腥,我揣著相機(jī)與錄音,去河邊找鬼害淤。 笑死扇雕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窥摄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼础淤,長吁一口氣:“原來是場噩夢啊……” “哼崭放!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸽凶,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤币砂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后玻侥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體决摧,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掌桩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片边锁。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖波岛,靈堂內(nèi)的尸體忽然破棺而出茅坛,到底是詐尸還是另有隱情,我是刑警寧澤则拷,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布贡蓖,位于F島的核電站,受9級特大地震影響煌茬,放射性物質(zhì)發(fā)生泄漏斥铺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一坛善、第九天 我趴在偏房一處隱蔽的房頂上張望仅父。 院中可真熱鬧,春花似錦浑吟、人聲如沸笙纤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽省容。三九已至,卻和暖如春燎字,著一層夾襖步出監(jiān)牢的瞬間腥椒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工候衍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笼蛛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓蛉鹿,卻偏偏與公主長得像滨砍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子妖异,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容

  • ??Google爸爸在今年(2020年)的Jetpack庫里面更新paging組件惋戏,推出了Paing3。按照Goo...
    瓊珶和予閱讀 3,194評論 5 12
  • ??距離上一篇Jetpack源碼分析的文章已經(jīng)兩個(gè)月他膳,時(shí)間間隔確實(shí)有點(diǎn)長响逢。最近,感覺自己的學(xué)習(xí)積極性不那么的高棕孙,看...
    瓊珶和予閱讀 1,916評論 2 4
  • 技術(shù)不止舔亭,文章有料些膨,加 JiuXinDev 入群,Android 搬磚路上不孤單 前言 又到了學(xué)習(xí) Android...
    九心_閱讀 12,312評論 5 19
  • 上個(gè)周末晚上看到了鴻洋大神的公眾號推送文章<<Jetpack重磅更新>>钦铺,于是乎點(diǎn)開文章看了一下具體內(nèi)容订雾,在翻閱的...
    Colaman丶閱讀 1,387評論 0 2
  • 久違的晴天,家長會职抡。 家長大會開好到教室時(shí)葬燎,離放學(xué)已經(jīng)沒多少時(shí)間了。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)缚甩。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,523評論 16 22