??Google爸爸在今年(2020年)的Jetpack庫里面更新paging組件怕午,推出了Paing3获列。按照Google爸爸文檔的描述,Paing3完全使用的是kotlin似枕,其中還包括了kotlin 的很多特性溯警,比如說協(xié)程趣苏,F(xiàn)low和Channel等狡相。出于好奇梯轻,想要了解其使用方式和內(nèi)部實現(xiàn)原理,因此尽棕,便寫下這篇文章喳挑。
??其實,Paging3的推出時間對于我來說挺尷尬滔悉,Paging3推出的第一個版本時伊诵,那個時候我在看Paging2的源碼。當(dāng)時在學(xué)習(xí)Paging2的時候回官,內(nèi)心一直在想曹宴,我還有必要學(xué)習(xí)這個嗎?不得不說歉提,Google爸爸推陳出新的速度太快了笛坦,都來不及追隨了。
??好了苔巨,不廢話了版扩,開始本文的主題吧。本文打算由淺入深的分析Paging3侄泽,從最開始的基本使用講到內(nèi)部實現(xiàn)原理礁芦。主要內(nèi)容如下:
- 基本使用。包括PagingSource的使用悼尾,RemoteMediator的使用柿扣。
- 基本架構(gòu)。Paging3內(nèi)部定義了很多的類闺魏,在分析原理之前窄刘,我們需要理解清楚每個類的含義,以及類之間是怎么串聯(lián)起來舷胜。這些理解清楚了娩践,看代碼的時候才不會疑惑活翩。
- 首次加載的過程。主要是分析Paging怎么實現(xiàn)第一次Refresh操作的翻伺。
- 加載更多的過程材泄。主要是分析Pagin怎么去加載上一頁或者下一頁的數(shù)據(jù)。
- RemoteMediator的實現(xiàn)原理吨岭。了解Paging的同學(xué)應(yīng)該都知道拉宗,RemoteMediator主要是將網(wǎng)絡(luò)上的數(shù)據(jù)放在緩存在數(shù)據(jù)庫里,以保證無網(wǎng)的狀態(tài)下也能加載數(shù)據(jù)辣辫。由于RemoteMediator的實現(xiàn)原理跟普通單一數(shù)據(jù)源加載方式有很大不同的旦事,所以我單獨拎出來分析。(下篇內(nèi)容)
??同時介于篇幅的原因急灭,將本文的內(nèi)容拆分上下兩篇姐浮,本文是上篇內(nèi)容,下篇內(nèi)容主要講解RemoteMediator
葬馋。
??在閱讀本文之前卖鲤,默認(rèn)大家已經(jīng)掌握如下的知識,本文不做單獨介紹:
- Kotlin 的協(xié)程畴嘶,F(xiàn)low和Channel蛋逾。
- Room的基本使用。
??本文參考資料:
??注意窗悯,本文Paging源碼均來自于3.0.0-alpha08版本区匣。
1. 概述
??Paging組件本身的作用本文就不做過多的介紹,有興趣的同學(xué)可以看看:Jetpack 源碼分析(四) - Paging源碼分析蒋院。在這里亏钩,我介紹一下Paging3和Paging2的不同。
- Adapter從
PagedListAadpter
更換成為了PagingDataAdapter
悦污。- 廢棄了
PagedList
铸屉,內(nèi)部使用PagingData
和PagePresenter
來存儲。網(wǎng)上有人說PagingData
替代了PagedList
切端,我覺得不太準(zhǔn)確彻坛。因為在Paging2中,PagedList只會創(chuàng)建一次踏枣,那就是在Refresh的時候昌屉,同時PagedList還兼有數(shù)據(jù)存儲和處理,存儲了已加載所有的數(shù)據(jù)茵瀑;而在Paging3里面间驮,PagingData只存儲一次Refresh + 多次Prepend + 多次Append的數(shù)據(jù),之所以這么說马昨,因為在Paging3中竞帽,即使不手動Refresh扛施,也會可能會Refresh,這個主要是跟RemoteMediator
有關(guān)屹篓。所以在Paging3
中疙渣,PagingData
只是用來提交數(shù)據(jù),而不是存儲數(shù)據(jù)的堆巧,存儲和處理數(shù)據(jù)主要是由PagePresenter
妄荔。- 簡化了數(shù)據(jù)加載的邏輯。在Paging2中谍肤,我們通過自定義DataSource來實現(xiàn)加載啦租,定義的時候需要考慮初始化加載,以及更多加載的區(qū)別荒揣,同時還需要實現(xiàn)不同DataSource篷角,來區(qū)分不同場景的數(shù)據(jù);而在Paging3中乳附,只需要定義
PagingSource
負(fù)責(zé)加載數(shù)據(jù)即可内地。注意伴澄,我們不能理解為DataSource已經(jīng)過時了赋除,一是Google爸爸并沒有把這個類標(biāo)記為過時,其次在RemoteMediator
內(nèi)部還在使用它非凌。
??Paging3內(nèi)部實現(xiàn)完全使用Kotlin實現(xiàn)举农,其中使用Flow和Channel替代了LiveData,通過協(xié)程實現(xiàn)異步處理敞嗡。作為使用者颁糟,我喜歡這種設(shè)計,因為更加簡潔喉悴;但是棱貌,做了開發(fā)者和源碼閱讀者來說,增加很多的工作量箕肃。從兩個方面來說:
- 使用Flow和Channel實現(xiàn)監(jiān)聽婚脱。Flow的上游來源在下游是不可知的,如果下游出現(xiàn)問題勺像,沒法追溯障贸,只能愣看代碼。
- 使用協(xié)程進(jìn)行異步處理吟宦。debug難度增加篮洁,就目前而言,debug Paging3內(nèi)部的源碼存在兩個問題殃姓,要么打了debug的斷點根本不生效袁波,要么就是雖然生效了瓦阐,但是始終斷不下來。
??例如篷牌,打了debug的斷點根本不生效垄分。我想看一下PageFetcherSnapshot
的doLoad
方法調(diào)用情況,發(fā)現(xiàn)打了斷點根本不生效:
??在另外一個地方打了斷點娃磺,雖然生效了薄湿,但是根本斷不下來:
??大家看上面動圖,我在前后打了兩個斷點偷卧,前面那個斷點不能斷豺瘤,后面那個斷點可以斷下來。真的听诸,看Paging3的源碼真的太難了坐求,連普通的debug都成問題。
??不過晌梨,抱怨歸抱怨桥嗤,源碼我們還是要看的。
2. 基本使用
??在分析原理實現(xiàn)之前仔蝌,我們先來看看Paging3的基本使用泛领。Paging的使用主要分為兩個部分:單一數(shù)據(jù)源和多級數(shù)據(jù)源。
(1). 單一數(shù)據(jù)源
??在Paging3中敛惊,PagingSource
就是負(fù)責(zé)單一數(shù)據(jù)源的加載渊鞋。那么什么是單一數(shù)據(jù)源呢?我的理解是瞧挤,只有一個來源的數(shù)據(jù)就是單一數(shù)據(jù)源锡宋,比如說我從PagingSource中獲取數(shù)據(jù),只能來自于一個地方特恬,要么網(wǎng)絡(luò)执俩,要么本地,這個就是所謂單一數(shù)據(jù)源癌刽。我們來看看役首,是怎么使用的吧,主要分為三步:
- 定義一個
PagingSource
妒穴,并且使用ViewModel 將提供給Activity/Fragment宋税。- 自定義一個RecyclerView的Adapter,繼承于
PagingDataAdapter
- Activity/Fragment從ViewModel獲取一個Flow,并且監(jiān)聽讼油,同時將監(jiān)聽的到內(nèi)容通過Adapter的
submitData
方法提交給它杰赛。
??我們來看看具體的代碼實現(xiàn)。
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的定義就變得非常簡單了乏屯,只需要我們實現(xiàn)一個方法就行了根时,不過這里面我們需要注意兩個點:
- load方法的返回值是一個LoadResult對象。其中如果請求成功的話辰晕,需要返回
LoadResult.Page
;如果請求失敗蛤迎,需要返回LoadResult.Error
對象,這一點大家一定要注意含友。在這里替裆,我特意的分析一下LoadResult.Page
內(nèi)部幾個參數(shù)含義,其內(nèi)部幾個參數(shù)含義如下:
參數(shù) | 含義 |
---|---|
data | 加載的數(shù)據(jù)列表窘问。 |
prevKey | 上一頁數(shù)據(jù)的key辆童。如果為空的話,表示沒有上一頁數(shù)據(jù)惠赫。 |
nextKey | 下一頁數(shù)據(jù)的key把鉴。如果為空的話,表示沒有下一頁數(shù)據(jù)儿咱。 |
itemsBefore | 當(dāng)前加載完畢頁之前需要加載占位符的數(shù)量庭砍,默認(rèn)值是COUNT_UNDEFINED ,表示不會創(chuàng)建占位符混埠。比如說怠缸,當(dāng)前加載的是第一頁的數(shù)據(jù),itemsBefore 為100的話岔冀,那么在這頁數(shù)據(jù)之前凯旭,會有100個占位符概耻,這個我們也可以從 Adapter的itemCount驗證使套,這里就不展示。同時鞠柄,這里也不解釋什么是 占位符侦高,感興趣的同學(xué)可以參考:Jetpack 源碼分析(四) - Paging源碼分析 |
itemsAfter | 同itemsBefore ,表示當(dāng)前加載完畢頁之后需要加載占位符的數(shù)量厌杜。 |
- 在加載數(shù)據(jù)完成之后返回
LoadResult.Page
奉呛,注意設(shè)置prevKey
和nextKey
的值,否則有可能不會自動加載下一頁的數(shù)據(jù)夯尽。
B. 定義ViewModel并且暴露Flow
??在Paging3中瞧壮,Google爸爸推薦使用Flow替代LiveData,用來給UI層傳遞數(shù)據(jù)匙握。這里為了簡單起見咆槽,我們就不獨樹一幟,按照Google爸爸推薦來定義圈纺∏胤蓿看看代碼:
class NetWorkViewModel : ViewModel() {
val messageFlow = Pager(PagingConfig(20)) {
CustomPagingSource()
}.flow.cachedIn(viewModelScope)
}
??如上便是ViewModel定義的完整過程麦射,非常的簡單,不過這里我們需要注意如下:
- Flow是從Pager里面拿到的灯谣,其中它有幾個參數(shù)潜秋,我們需要注意一下,如下:
參數(shù) | 含義 |
---|---|
config | Paging 的配置項胎许,這個配置項我們在Paging2里面已經(jīng)解釋過了 峻呛,這里就不過多的解釋,只解釋新增幾個配置參數(shù)辜窑。 initialLoadSize 杀饵,表示初始化加載需要加載的數(shù)據(jù)量,默認(rèn)是pageSize * 3,針對于這 個參數(shù)我們特別要跟 pageSize 區(qū)分開來谬擦,雖然我們設(shè)置了pageSize 鳍置,但是如果沒有設(shè)置 initialLoadSize 床未,第一次加載的數(shù)據(jù)量是pageSize * 3 ;jumpThreshold ,根據(jù)Google爸爸注釋和實現(xiàn)的代碼兔综,大概的意思是,當(dāng)滑動到了邊界锄蹂,并且加載的數(shù)據(jù)量超過了設(shè)置的閾值了袁,那么就觸發(fā)refresh邏 輯,此字段我會在分析 RemoteMediator 里面分析贺氓。 |
initialKey | 初始加載的key蔚叨。 |
remoteMediator | 多級數(shù)據(jù)源請求的工具類,這個類的意思后續(xù)我會專門的介紹辙培, 這里先不介紹了蔑水。 |
pagingSourceFactory | 用來創(chuàng)建一個PagingSource。 |
- 我們將flow對象扬蕊,通過
cachedIn
方法緩存在一個viewModelScope
里面搀别。根據(jù)Google爸爸的介紹,這樣可以在Activity重建的時候尾抑,F(xiàn)low里面已經(jīng)轉(zhuǎn)換過的數(shù)據(jù)不會再次轉(zhuǎn)換歇父,而是直接拿來用。
??如上便是ViewModel的定義再愈,是不是很簡單了呢榜苫?接下來,我們再來看看View層是怎么監(jiān)聽的翎冲。
C. View層監(jiān)聽Flow
??首先View層要想監(jiān)聽Flow的回調(diào)垂睬,RecyclerView 的Adapter必須繼承于PagingDataAdapter
。關(guān)于Adapter的定義,這里就不單獨的講解羔飞,相信大家都非常的熟悉肺樟。
??我們直接來看代碼:
lifecycleScope.launchWhenCreated {
mMessageFlow.collectLatest {
adapter.submitData(it)
}
}
??上面展示的是Adapter監(jiān)聽Flow的數(shù)據(jù)變化,其中逻淌,我們需要注意如下的內(nèi)容:
collectLatest
方法是掛起函數(shù)么伯,所以必須在協(xié)程里面調(diào)用。lifecycleScope
是Google爸爸給我們提供生命周期敏感的協(xié)程作用卡儒,保證頁面銷毀時協(xié)程能正常取消田柔。
(2).多級數(shù)據(jù)源
??單一數(shù)據(jù)源比較簡單,數(shù)據(jù)來源只有一個地方骨望。實際上硬爆,在真實項目開發(fā)過程中,數(shù)據(jù)來源并沒有那么簡單擎鸠,很常見的場景是:本地數(shù)據(jù)庫 + 網(wǎng)絡(luò)數(shù)據(jù)缀磕。如果手機沒有網(wǎng)絡(luò),可以使用本地數(shù)據(jù)庫顯示數(shù)據(jù)劣光,以保證用戶在無網(wǎng)絡(luò)的時候能正常使用App袜蚕。
??出于這種情況的考慮,Google爸爸在設(shè)計Paging3時绢涡,新增加了一個工具類RemoteMediator
,用來實現(xiàn)多級數(shù)據(jù)源牲剃。我們來看看具體代碼實現(xiàn)(如下代碼涉及到Room庫,這里就不單獨解釋雄可,默認(rèn)大家都懂)凿傅。
??首先使用Room定義一個Dao,用以對數(shù)據(jù)庫操作数苫,通常來說Dao里面必須含有三個操作
- 查詢聪舒,需要返回一個
PagingSource
對象。- 插入文判,當(dāng)有新數(shù)據(jù)过椎,我們需要同步到數(shù)據(jù)庫中去,以備后用戏仓。
- 清空所有數(shù)據(jù),當(dāng)用戶刷新操作成功之后亡鼠,應(yīng)該清空之前所有的數(shù)據(jù)赏殃,然后才插入新的數(shù)據(jù)。
@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對象。如果大家發(fā)現(xiàn)Room生成的代碼不支持PagingSource
類型,可以將Room升級到2.3.0版本抗蠢。
??其次就是實現(xiàn)RemoteMediator
接口举哟,用以從網(wǎng)絡(luò)獲取數(shù)據(jù),我們直接來看具體的實現(xiàn):
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
比較類似迅矛,都有l(wèi)oad方法妨猩,只不過他們所做的事情不太一樣,RemoteMediator
的load方法做了兩件事:
- 通過不同的loadType獲取key秽褒,這個key可能是prevKey壶硅,也可能是nextKey。
- 通過拿到的key從網(wǎng)絡(luò)上請求數(shù)據(jù)销斟,并且放到數(shù)據(jù)庫中去庐椒。需要注意的是,這里并并沒有將請求的數(shù)據(jù)結(jié)果通過result返回去蚂踊,那么是因為
RemoteMediator
的職責(zé)是從網(wǎng)絡(luò)上請求數(shù)據(jù)约谈,然后放到數(shù)據(jù)庫里面,這一點跟PagingSource
有很大的不同犁钟。那么RemoteMediator
請求回來時怎么提交給到Adapter呢窗宇?這個我們在后續(xù)的內(nèi)容會重點分析。
??最后就是ViewModel里面的定義特纤,我們直接來看代碼:
class NetWorkAndDataBaseViewModel : ViewModel() {
@ExperimentalPagingApi
val messageFlow = Pager(PagingConfig(20), remoteMediator = CustomRemoteMediator()) {
DataBaseHelper.dataBase.messageDao().getMessage()
}.flow.cachedIn(viewModelScope)
}
??多級數(shù)據(jù)源跟單一數(shù)據(jù)源關(guān)于Flow的創(chuàng)建不太一樣军俊,主要體現(xiàn)在如下兩點:
- Pager的構(gòu)造方法里面需要傳入一個
RemoteMediator
對象,這個就是我們自定義的RemoteMediator
捧存。- 其次粪躬,PagingSource不需要我們自定義,直接從我們之前定義的Dao里面獲取就行昔穴。Room會通過代碼生成的方式镰官,返回一個指定的PagingSource對象。
??關(guān)于多級數(shù)據(jù)源的其地方都跟單一數(shù)據(jù)源都是一樣的吗货,這里就不再贅述了泳唠。
(3). 代碼
??為了大家方便理解,我將我的Demo代碼上傳到github:KotlinDemo宙搬。同時額外的說一句笨腥,后續(xù)我會將所有的Demo代碼匯總在這個工程里面,方便大家參考學(xué)習(xí)勇垛。
3. 基本架構(gòu)
??接下來脖母,我們將進(jìn)入源碼分析階段。不過在這之前闲孤,我們先來了解一下Paging3內(nèi)部整個架構(gòu)谆级,方便后續(xù)在源碼分析的時候,腦海中先有一個輪廓,不至于懵逼肥照。
??根據(jù)我對Paging3框架的理解脚仔,我將Paging3內(nèi)部實現(xiàn)分為兩個部分:
- 數(shù)據(jù)請求層。這一層主要負(fù)責(zé)的是數(shù)據(jù)請求舆绎,其中包括加載初始化的數(shù)據(jù)鲤脏,加載更多的數(shù)據(jù),以及多級數(shù)據(jù)源的請求亿蒸。這部分的內(nèi)容主要是由
PageFetcher
凑兰,PageFetcherSnapshot
,PagingSource
边锁,RemoteMediator
等組成姑食。- 數(shù)據(jù)處理和顯示層。這一層主要是負(fù)責(zé)拿到數(shù)據(jù)請求層請求回來的數(shù)據(jù)茅坛,然后進(jìn)行處理和顯示音半。這部分的內(nèi)容主要是由
PagingDataAdapter
,AsyncPagingDataDiffer
贡蓖,PagingDataDiffer
和PagePresenter
??我們來分開看一下每一層主要架構(gòu)和聯(lián)系曹鸠。
(1). 數(shù)據(jù)請求層
??數(shù)據(jù)請求層最主要的責(zé)任就是數(shù)據(jù)請求,而數(shù)據(jù)請求的類型在Paging3分為兩種:
- 初始化頁面數(shù)據(jù)請求斥铺。
- 上一頁或和下一頁數(shù)據(jù)請求彻桃。
??這其中,由PageFetcher
提供Api觸發(fā)請求晾蜘,比如說PageFetcher
里面有refresh
和invalidate
兩個方法可以觸發(fā)刷新邏輯邻眷;而更多數(shù)據(jù)請求是通過PageFetcher
的PagerUiReceiver
來首先觸發(fā)。整體邏輯是:由PageFetcher
內(nèi)部的Flow創(chuàng)建一個PageFetcherSnapshot
對象剔交,數(shù)據(jù)請求的操作通過PageFetcher
傳遞到PageFetcherSnapshot
里面肆饶,PageFetcherSnapshot
內(nèi)部有兩個方法來請求數(shù)據(jù),分別是:
- doInitialLoad:表示加載刷新的數(shù)據(jù)岖常。
- doLoad:表示加載其他頁的數(shù)據(jù)驯镊。
??所有的數(shù)據(jù)請求都會走到這兩個方法進(jìn)行數(shù)據(jù)請求,PagingSource
和RemoteMediator
的load方法也是在這兩個方法里面進(jìn)行調(diào)用的竭鞍。
(2). 數(shù)據(jù)處理和顯示層
??刷新請求完成之后板惑,PageFetcher
會通過內(nèi)部的Flow發(fā)送一個PagingData
對象。而Adapter會通過監(jiān)聽拿到這個PagingData笼蛛,然后進(jìn)行數(shù)據(jù)處理洒放。這其中PagingData內(nèi)部封裝了幾個參數(shù):
PageEvent
:內(nèi)部封裝了關(guān)于數(shù)據(jù)的信息。包括當(dāng)前加載類型滨砍,即LoadType
(REFRESH,PREPEND,APPEND)惋戏;加載狀態(tài)领追,即CombinedLoadStates
(NotLoading,Loading响逢,Error)绒窑;以及更重要的數(shù)據(jù)列表。UiReceiver
:用來觸發(fā)加載其他頁面數(shù)據(jù)的接口舔亭。
??Adapter拿到這個PaingData
對象之后些膨,會一路透傳,直到PagingDataDiffer
的collectFrom
方法钦铺。在PagingDataDiffer
內(nèi)部订雾,首先會通過PagingData
內(nèi)部的一個Flow監(jiān)聽PageEvent,然后不同類型的PageEvent(Insert矛洞,Drop洼哎,LoadStateUpdate)進(jìn)行分發(fā),如果是LoadStateUpdate
那么表示是加載狀態(tài)更新的沼本,即會回調(diào)加載狀態(tài)的監(jiān)聽Listener噩峦;如果是其他類型的PageEvent就進(jìn)行數(shù)據(jù)處理,數(shù)據(jù)處理主要是依靠PagePresenter來幫忙抽兆,然后將對應(yīng)的數(shù)據(jù)變化同步到Adapter層面上识补,即調(diào)用Adapter的notify方法。
(3). 枚舉類
??在Paging3的內(nèi)部辫红,有很多的枚舉類凭涂,用來表示某種狀態(tài)。如果我們在看源碼對這些枚舉類不了解厉熟,那么代碼理解起來就比較麻煩导盅,所以我在這里重點解釋一下。
A.LoadType
??即加載類型揍瑟,一共有三個枚舉類白翻,具體名字和含義如下:
名字 | 含義 |
---|---|
REFRESH | 刷新 |
PREPEND | 表示加載上一頁數(shù)據(jù) |
APPEND | 表示加載下一頁數(shù)據(jù) |
B.LoadState
??加載狀態(tài),即當(dāng)前加載是什么一個情況绢片,通常來說滤馍,每一種LoadType都對應(yīng)一個LoadState,表示當(dāng)前加載類型的具體狀態(tài)底循。具體名字和含義如下:
名字 | 含義 |
---|---|
NotLoading | 表示沒有在加載或者已經(jīng)加載完成巢株。內(nèi)部帶有一個標(biāo)記字段, 即 endOfPaginationReached ,用來表示當(dāng)前是否還有剩余的數(shù)據(jù)需要加載熙涤,其中false表示還有剩余的數(shù)據(jù)未加載阁苞,true表示沒有 剩余的數(shù)據(jù)困檩。比如說APPEND的狀態(tài)為NotLoading,表示當(dāng)前 加載更多已經(jīng)完成那槽,如果 endOfPaginationReached 為false悼沿,表示還有數(shù)據(jù)需要加載,到了合適的時機還有觸發(fā)加載更多骚灸,反之亦然糟趾。 |
Loading | 表示正在加載。 |
Error | 表示加載失敗甚牲。 |
C.PageEvent
??最終數(shù)據(jù)請求的結(jié)果都會作為PageEvent
通過PagingData
傳遞到UI層义郑,PageEvent一共三個枚舉狀態(tài),分別如下:
名字 | 含義 |
---|---|
Insert | 表示有新的數(shù)據(jù)增加丈钙,其中Refresh ,Append ,Prepend 請求成功之后都會產(chǎn)生這個事件非驮。 |
Drop | 表示有數(shù)據(jù)需要刪除,這個事件是在Append 和Prepend 時機才會產(chǎn)生著恩。 |
LoadStateUpdate | 表示每種加載類型的加載狀態(tài)更新院尔。 |
??除去這些枚舉類,Paging3內(nèi)部還有各種Helper類喉誊,用來存儲狀態(tài)邀摆,這里就不再過多的介紹,在源碼分析過程中伍茄,如果遇到栋盹,我會進(jìn)行簡單的解釋。
4. 首次加載
??接下來敷矫,我們將進(jìn)入源碼分析階段例获,首先我們來看看首次加載的過程,需要注意的是這里的首次加載泛指進(jìn)入頁面的第一次加載和手動刷新加載曹仗。這里我從兩個方面分析首次加載的過程榨汤,分別是:數(shù)據(jù)請求層和數(shù)據(jù)處理和顯示層。首先我們來看一下數(shù)據(jù)請求層的實現(xiàn)怎茫。
(1). 數(shù)據(jù)請求層
??我們在介紹基本使用的時候已經(jīng)提到過收壕,我們需要通過Pager拿到一個Flow對象,Pager的Flow對象其實是從PageFetcher
里面獲取的轨蛤,所以我們直接來看PageFetcher
里面實現(xiàn)蜜宪。
??在正式介紹源碼之前,我們先來看一下PageFetcher
內(nèi)部的兩個Channel對象:
refreshChannel
:用來通知觸發(fā)刷新邏輯祥山。其中true表示RemoteMediator
也要刷新(如果有的話)圃验,false則表示RemoteMediator
不刷新。retryChannel
:用重試刷新缝呕。我們通過Adapter的retry方法澳窑,最終會通過這個Channel來通知重試斧散。
??接下來,我們來看一下PageFetcher
內(nèi)部最重要的一個Flow的實現(xiàn):
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) }
}
??初次看這段代碼照捡,可能會有點懵逼颅湘,最初我也是這樣的话侧。不過栗精,大家不用擔(dān)心,我會給大家介紹這個Flow里面做了哪些事情瞻鹏。我們先從宏觀上來看這段代碼都做啥事吧(這里為了簡單悲立,我們先把RemoteMediator
相關(guān)的忽略,后續(xù)有內(nèi)容專門來介紹它新博。)薪夕,分別:
- 在scan方法里面,創(chuàng)建了
PageFetcherSnapshot
對象赫悄。主要分為三步:首先原献,通過傳入進(jìn)來的工廠函數(shù)創(chuàng)建一個PagingSource,同時如果還有之前的PageFetcherSnapshot
存在埂淮,需要進(jìn)行一些清理工作(scan方法內(nèi)部有一個size 為1的Buffer姑隅,會緩存上一個PageFetcherSnapshot
對象);其次調(diào)用refreshKeyInfo
方法拿到刷新的key倔撞;最后就是創(chuàng)建了一個PageFetcherSnapshot
讲仰,同時給PageFetcherSnapshot
傳入可能會用到的參數(shù)。- 通過map函數(shù)將相關(guān)事件轉(zhuǎn)為成為一個
PagingData
對象痪蝇,同時還有PagingData傳入兩個參數(shù)鄙陡,分別是一個PageEvent的Flow對象,下游(UI 層)可以用來監(jiān)聽PageEvent 的發(fā)送躏啰;其次趁矾,就是構(gòu)建了一個PagerUiReceiver
對象,用來給下游(UI 層)來觸發(fā)加載下一頁數(shù)據(jù)和嘗試重試加載给僵。- 通過send方法將創(chuàng)建好的PagingData發(fā)送出去的毫捣。
??我們都知道,F(xiàn)low是冷流想际,即只有在收集的時候才會觸發(fā)上面一系列的流程培漏。所以我們在Activity/Fragment 里面調(diào)用Flow的collectLatest
方法自然觸發(fā)了上面流程,從而開始初始化加載胡本。
??關(guān)于上面的代碼牌柄,大家還有需要注意一點的是,PagingData的PageEvent是從PageFetcherSnapshot
獲取的侧甫,我們在前面介紹過珊佣,PageFetcherSnapshot
的工作主要負(fù)責(zé)加載數(shù)據(jù)蹋宦,同時將加載完成的數(shù)據(jù)通過發(fā)送一個PageEvent來通知到下游。
??我們接下來看一下PageFetcherSnapshot
的pageEventFlow
參數(shù)咒锻,因為所有的事件都是通過它發(fā)送到下游去的冷冗。
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)
// ......(監(jiān)聽下游發(fā)送的過來的事件,嘗試加載上一頁或者下一頁刷劇)
}
??pageEventFlow
的代碼較多惑艇,我刪除一些我們現(xiàn)在不用關(guān)心的代碼蒿辙,避免影響我們分析整個流程。我們從上面已有的代碼滨巴,我們看出來幾點:
- pageEventCh是用來發(fā)送PageEvent的思灌,這里發(fā)送的PageEvent會直接到達(dá)UI 層。不過需要注意的是恭取,這里發(fā)送的PageEvent不僅僅是Insert泰偿,還有其他類型的(Drop和LoadStateUpdate)。
- 調(diào)用了
doInitialLoad
方法蜈垮,進(jìn)行數(shù)據(jù)請求耗跛。
??接下來我們來看doInitialLoad
方法的實現(xiàn)。先直接看代碼:
private suspend fun doInitialLoad(
state: PageFetcherSnapshotState<Key, Value>
) {
// 設(shè)置狀態(tài)攒发,當(dāng)前正在刷新调塌。
stateLock.withLock { state.setLoading(REFRESH) }
val params = loadParams(REFRESH, initialKey)
// 調(diào)用pagingSource的load 方法,進(jìn)行網(wǎng)絡(luò)請求
when (val result = pagingSource.load(params)) {
is Page<Key, Value> -> {
// 將請求的結(jié)果插入到PageFetcherSnapshotState里面
val insertApplied = stateLock.withLock { state.insert(0, REFRESH, result) }
// 更新loadType 對應(yīng)的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層進(jìn)行數(shù)據(jù)已經(jīng)更新晨继。
if (insertApplied) {
stateLock.withLock {
with(state) {
pageEventCh.send(result.toPageEvent(REFRESH))
}
}
}
// ......(remoteMediator的調(diào)用烟阐,先忽略)
}
is LoadResult.Error -> stateLock.withLock {
val loadState = Error(result.throwable)
if (state.setSourceLoadState(REFRESH, loadState)) {
pageEventCh.send(LoadStateUpdate(REFRESH, false, loadState))
}
}
}
}
??doInitialLoad
方法所做事情比較簡單,我們來看一下:
- 首先調(diào)用
PageFetcherSnapshotState
的setLoading方法表示當(dāng)前正在刷新紊扬,在setLoad方法里面會通過pageEventCh
發(fā)送一個LoadStateUpdate
事件蜒茄,來通知UI 層加載狀態(tài)已經(jīng)變化了。- 調(diào)用PagingSource的load 方法餐屎,進(jìn)行數(shù)據(jù)請求檀葛。這個數(shù)據(jù)可能是從網(wǎng)絡(luò)上請求數(shù)據(jù),也有可能是從數(shù)據(jù)庫里面請求數(shù)據(jù)腹缩,具體得看PagingSource的定義屿聋。
- 根據(jù)
load
返回的結(jié)果進(jìn)行分情況討論。如果是Error
藏鹊,那么就會給下游發(fā)送請求失敗的PageEvent润讥;如果是請求成功,即返回的是Page
盘寡,那么就分為幾步來進(jìn)行楚殿。首先是,將請求的結(jié)果存儲到PageFetcherSnapshotState
里面去竿痰;其次返回結(jié)果傳入的nextKey和prevKey來更新每個LoadType下的LoadState脆粥,以保證后續(xù)的加載更多能夠正常進(jìn)行砌溺。- 發(fā)送一個PageEvent,通知UI層數(shù)據(jù)更新变隔。
??doInitialLoad
方法所做之事便如上內(nèi)容规伐,在這里,大家發(fā)現(xiàn)了一個PageFetcherSnapshotState
類匣缘,肯定有疑惑這個類是干嘛的猖闪,我在這里簡單的解釋一下。
??通過官方的注釋孵户,我們可以知道這個類主要是來記錄PageFetcherSnapshot
的狀態(tài),那么記錄都是什么狀態(tài)呢萧朝?
- 數(shù)據(jù)相關(guān)的信息。
PageFetcherSnapshotState
內(nèi)部有一個_pages
變量夏哭,記錄的是已經(jīng)加載的頁面數(shù)據(jù),這個我們從doInitialLoad
方法里面也可以看到献联,請求完成之后會調(diào)用PageFetcherSnapshotState
的方法
竖配,目的就是將數(shù)據(jù)存儲到PageFetcherSnapshotState
。還有其他數(shù)據(jù)相關(guān)信息里逆,比如說當(dāng)前占位符的數(shù)量进胯,即placeholdersBefore
和placeholdersAfter
,這個變量跟我們之前在介紹LoadResult.Page
的itemsBefore
和itemsBefore
是同一個意思原押。以及還有其他信息胁镐,這里就不過多的介紹。- 每種LoadType對應(yīng)的LoadState诸衔。內(nèi)部有一個
sourceLoadStates
變量盯漂,記錄三種LoadType 的狀態(tài)。外部通常通過setSourceLoadState
來更新對應(yīng)的值笨农,同理就缆,我們可以在doInitialLoad
方法看到它被調(diào)用的影子。
(2). 數(shù)據(jù)處理和顯示層
??我們從上面知道了首次加載的數(shù)據(jù)會通過發(fā)送一個PageEvent傳送到數(shù)據(jù)處理和顯示層(即UI 層谒亦,為了簡單竭宰,后文統(tǒng)一使用UI層表示)。
??繁瑣的源碼追蹤工作份招,我們這里不做了切揭,我們直接到PagingDataDiffer
的collectFrom
方法里面去,因為在方法里面對PageEvent事件進(jìn)行監(jiān)聽锁摔。我們直接看代碼:
suspend fun collectFrom(pagingData: PagingData<T>) = collectFromRunner.runInIsolation {
// 存下UiReceiver廓旬,以備后續(xù)觸發(fā)加載更多。
receiver = pagingData.receiver
pagingData.flow.collect { event ->
withContext<Unit>(mainDispatcher) {
// 如果是Insert鄙漏,并且是刷新操作。
if (event is PageEvent.Insert && event.loadType == REFRESH) {
lastAccessedIndexUnfulfilled = false
// 創(chuàng)建一個PagePresenter用以存儲和處理數(shù)據(jù)
val newPresenter = PagePresenter(event)
// 將舊PagePresenter里面數(shù)據(jù)遷移到新的PagePresenter
val transformedLastAccessedIndex = presentNewList(
previousList = presenter,
newList = newPresenter,
newCombinedLoadStates = event.combinedLoadStates,
lastAccessedIndex = lastAccessedIndex
)
presenter = newPresenter
// 回調(diào)Listener
dataRefreshedListeners.forEach { listener ->
listener(event.pages.all { page -> page.data.isEmpty() })
}
dispatchLoadStates(event.combinedLoadStates)
// 嘗試觸發(fā)加載下一頁(上一頁)的數(shù)據(jù)
transformedLastAccessedIndex?.let { newIndex ->
lastAccessedIndex = newIndex
receiver?.accessHint(
newPresenter.viewportHintForPresenterIndex(newIndex)
)
}
} else {
// Append 或者Prepend
}
}
}
}
??collectFrom
方法的代碼比較長瞎领,主要是處理兩部分的內(nèi)容:Refresh和非Refresh瓮增。這里,我們將非Refresh的代碼省略旁赊,只看Refresh部分的代碼。collectFrom
方法針對Refresh做了如下幾件事:
- 存下UiReceiver椅野,以備后續(xù)觸發(fā)加載更多终畅。這個我們說了很多遍,這里就不過多的說了竟闪,后續(xù)在介紹加載更多的過程時离福,會再次看到的。
- 創(chuàng)建一個新的新PagePresenter炼蛤,用來存儲和處理數(shù)據(jù)妖爷。
- 調(diào)用
presentNewList
方法,將舊的Presenter數(shù)據(jù)遷移到新的Presenter理朋。主要實現(xiàn)是通過DiffUtil來計算Diff絮识,進(jìn)而通知Adapter notify,有興趣的同學(xué)可以看看方法的實現(xiàn)嗽上,這里就不介紹了次舌。同時看到這個,可能有人會有疑惑兽愤,Refresh都是將原來的數(shù)據(jù)清空彼念,然后插入新的數(shù)據(jù),為啥要把老的數(shù)據(jù)遷移到新的數(shù)據(jù)里面呢浅萧?在平常中逐沙,我們對Refresh的理解是沒有問題的,但是在Paging3中惯殊,Refresh 操作不一定會清空老的數(shù)據(jù)酱吝,這一點一定記住。- 回調(diào)對應(yīng)的Listener土思。
- 嘗試觸發(fā)加載下一頁(下一頁數(shù)據(jù))的數(shù)據(jù)务热。
transformedLastAccessedIndex
表示將在老的數(shù)據(jù)里面的lastAccessedIndex
(上一次訪問的位置,在get方法記錄的)在新的數(shù)據(jù)中的位置己儒。如果當(dāng)前數(shù)據(jù)量還不夠當(dāng)前訪問崎岂,需要加載更多的數(shù)據(jù)以滿足要求。
??相信大家在這里看到一個新的類--PagePresenter
闪湾,想要知道這個類的作用是什么冲甘?我在這里簡單的解釋一下。
PagePresenter內(nèi)部存儲了Adapter所有的數(shù)據(jù),可以簡單的理解為時Adapter的Data List江醇。因為從源碼中中看出來濒憋,Adapter 獲取數(shù)據(jù)和計算當(dāng)前數(shù)據(jù)總數(shù)量都是從通過PagePresenter。同時陶夜,PagePresenter 還擔(dān)任著處理PageEvent的責(zé)任凛驮,因為內(nèi)部有一個
processEvent
方法,這個方法的作用根據(jù)不同的PageEvent進(jìn)行不同的處理条辟,其中Insert
表示要新增數(shù)據(jù)黔夭;Drop
表示要刪除數(shù)據(jù);LoadStateUpdate
表示要更新狀態(tài)。除此之外羽嫡,這個類還有很多有意思的東西本姥,比如說ViewportHint
的構(gòu)造等,這里就不過多的介紹了杭棵。
?UI層對Refrsh處理的過程便是如上的內(nèi)容婚惫。到此,對首次加載的整個流程的分析就結(jié)束了颜屠,在這里辰妙,我做一個小小的總結(jié),方便大家腦海中能把這部分的內(nèi)容的串起來甫窟。
首先,在UI層蛙婴,我們在從ViewModel 拿到一個Flow粗井,這個Flow對象是用來監(jiān)聽PagingData。正常來說街图,只有Refresh才會發(fā)送一個PagingData,Append 和Prepend 不會發(fā)送PagingData浇衬。
PagingDataDiffer
會從PagingData里面拿到一個發(fā)送PageEvent
的Flow,當(dāng)數(shù)據(jù)請求完成餐济,這個Flow會收到一個Insert
類型的事件耘擂,這個事件里面封裝了請求回來的數(shù)據(jù)。拿到這個PageEvent絮姆,會創(chuàng)建一個PagePresenter來存儲和處理數(shù)據(jù)醉冤,以及處理對應(yīng)的PageEvent。
其次篙悯,在數(shù)據(jù)請求層蚁阳,PageFetcherSnapshot
通過調(diào)用doInitialLoad
方法,進(jìn)而調(diào)用PagingSource
的load方法鸽照。load方法返回了一個Result螺捐,PageFetcherSnapshot
會將這個Result轉(zhuǎn)換成為一個PageEvent,發(fā)送給UI層。
5. 加載更多
??接下來定血,我們將分析另一個加載的過程--加載更多赔癌。為了簡單起見,本章節(jié)的內(nèi)容中以加載下一頁的數(shù)據(jù)表示加載更多澜沟,即Append操作灾票。
??其實,我們在分析首次加載的過程中倔喂,已經(jīng)涉及到了很多這部分的內(nèi)容铝条,當(dāng)然在這之前也留下很多的伏筆。這里席噩,我們就來詳細(xì)的看一下加載更多的內(nèi)容班缰。
??在首次加載中,我們是先介紹數(shù)據(jù)請求層的內(nèi)容悼枢,再介紹UI層的內(nèi)容埠忘。在本章節(jié)中,我們反向操作馒索,先介紹UI層的內(nèi)容莹妒,再介紹數(shù)據(jù)請求層的內(nèi)容。為啥要這樣呢绰上?因為首次加載是一個主動過程旨怠,不需要讓UI層自己觸發(fā)(嚴(yán)格來說,也是UI層自己觸發(fā)的)蜈块,而加載更多確實是被動過程鉴腻,需要Ui層自己去觸發(fā)。
(1). UI 層觸發(fā)
??總的來說百揭,觸發(fā)加載更多的地方很少爽哎,很簡單,我將其分為兩類:
- 調(diào)用Adapter的
getItem
方法器一,會嘗試加載更多的數(shù)據(jù)课锌。其中這個getItem方法的調(diào)用包括RecyclerView 自己調(diào)用,還有一個就是我們手動調(diào)用祈秕。所以渺贤,如果使用Paging3,不要隨意的調(diào)用getItem方法踢步,切記切記癣亚!- 正在請求的數(shù)據(jù)已經(jīng)回來,但是發(fā)現(xiàn)已有的數(shù)據(jù)不夠訪問位置的要求获印,會自己請求述雾。比如說街州,當(dāng)前我們訪問的index是100,數(shù)據(jù)請求回來發(fā)現(xiàn)才80條數(shù)據(jù)玻孟,還不夠數(shù)量唆缴,會繼續(xù)請求。
??關(guān)于第一點黍翎,我不用解釋什么面徽。但是第二個點,我們需要重點分析一下匣掸,了解它的細(xì)節(jié)趟紊。在PagingDataDiffer
內(nèi)部有兩個變量:
- lastAccessedIndex:表示上一次訪問的位置。
- lastAccessedIndexUnfulfilled:表示上一次訪問是否命中占位符碰酝。通俗來講就是霎匈,上一次訪問的位置是否超出已有數(shù)據(jù)的邊界,true表示超出邊界送爸,false表示沒有超出邊界铛嘱。但是從源碼來看,Google爸爸在
PagingDataDiffer
的get方法里面直接設(shè)置為true袭厂,所以可以理解這個變量應(yīng)該默認(rèn)每次訪問都會超出邊界墨吓。不過這樣讓人很疑惑,不知道Google是出于什么考慮纹磺。
??當(dāng)數(shù)據(jù)請求回來之后(包括Refresh和非Refresh)帖烘,會根據(jù)上一次的訪問位置會再次詢問是否還可以加載下一頁的數(shù)據(jù)。代碼如下:
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
// 嘗試請求下一頁數(shù)據(jù)
receiver?.accessHint(
newPresenter.viewportHintForPresenterIndex(newIndex)
)
}
} else {
// ·······
if (!canContinueLoading) {
// ·······
} else if (lastAccessedIndexUnfulfilled) {
// ·······
if (shouldResendHint) {
// 嘗試請求下一頁數(shù)據(jù)
receiver?.accessHint(
presenter.viewportHintForPresenterIndex(lastAccessedIndex)
)
} else {
// lastIndex fulfilled, so reset lastAccessedIndexUnfulfilled.
lastAccessedIndexUnfulfilled = false
}
}
}
}
}
}
}
??關(guān)于這里面的計算細(xì)節(jié)橄杨,我們就不細(xì)扣了蚓让,有興趣的同學(xué)可以去看看。不過這里讥珍,我們發(fā)現(xiàn)在調(diào)用UiReceiver
的accessHint
方法時,創(chuàng)建了一個ViewportHint
對象窄瘟,那么這個ViewportHint
對象有什么用呢衷佃?我們先來看一下這個類的幾個成員變量:
名字 | 含義 |
---|---|
pageOffset | 表示當(dāng)前訪問index所在的頁面index。我們都知道蹄葱,Paging 里面的數(shù)據(jù)都是分頁存儲氏义,類似于List<List<Data>>結(jié)構(gòu), 而這個 pageOffset 表示的時List<Data>的index图云。 |
indexInPage | 表示當(dāng)前訪問index在頁面內(nèi)的index惯悠。 |
presentedItemsBefore | 表示當(dāng)前訪問index之前非展位符的數(shù)量。比如說竣况,當(dāng)前訪 問位置是100克婶,當(dāng)時前置占位符有40個,那么 presentedItemsBefore 就等于60(100 - 40)。同時如果這個變量如果為0情萤,表示訪問 位置恰好是上邊界鸭蛙;如果是正數(shù),那么表示訪問位置正好在 數(shù)據(jù)范圍內(nèi)筋岛;如果是負(fù)數(shù)娶视,訪問位置就是占位符。 |
presentedItemsAfter | 同presentedItemsBefore 睁宰,只是presentedItemsAfter 表示的是下邊界肪获。 |
originalPageOffsetFirst | 當(dāng)前數(shù)據(jù)第一頁面的頁碼。不一定都為0柒傻,因為有maxSize 的存在孝赫,maxSize會丟棄前面已失效的數(shù)據(jù)(用null來填充)。 |
originalPageOffsetLast | 同originalPageOffsetFirst ,只是originalPageOffsetLast 表示最后一頁的頁面诅愚。 |
??關(guān)于上面的解釋寒锚,我猜測有些同學(xué)可能還會疑惑,我在這里在補充幾句违孝。
在Paging3內(nèi)刹前,存在三種index,分別是:
- 絕對index雌桑,我們可以這樣來理解這個index喇喉,就是將所有的數(shù)據(jù)拍平在一個List里面,每個item的Index就是絕對index校坑。
- 頁面index拣技,顧名思義,就是該頁面數(shù)據(jù)在所有頁面的index耍目。上述的
pageOffset
膏斤,originalPageOffsetFirst
,originalPageOffsetLast
都是頁面index邪驮。- 頁內(nèi)Index(相對index)莫辨,就是指該Item所在頁面里面內(nèi)的index。上述的
indexInPage
便是頁內(nèi)index毅访。
(2). 數(shù)據(jù)請求層沮榜。
??UI層通過調(diào)用UiReceiver
的accessHint
方法,并且通過一個ViewportHint
對象來攜帶一些信息喻粹。而accessHint
方法便是Ui層和數(shù)據(jù)請求層的溝通橋梁蟆融,數(shù)據(jù)請求層監(jiān)聽到這個方法的回調(diào),會向一個名為hintChannel
通道發(fā)送一個事件:
fun accessHint(viewportHint: ViewportHint) {
lastHint = viewportHint
@OptIn(ExperimentalCoroutinesApi::class)
hintChannel.offer(viewportHint)
}
注意上述的
accessHint
方法在PageFetcherSnapshot
里面守呜,調(diào)用關(guān)系:PagerUiReceiver#accessHint
->PageFetcherSnapshot#accessHint
型酥。
??那么哪里在監(jiān)聽這個事件呢山憨?是在startConsumingHints
方法里面。不過在看這個方法之前冕末,我們先回過頭來看一下pageEventFlow
的定義萍歉,之前我們看的時候省略了加載更多的代碼。
val pageEventFlow: Flow<PageEvent<Value>> = cancelableChannelFlow(pageEventChannelFlowJob) {
// ······
// 加載更多
if (stateLock.withLock { state.sourceLoadStates.get(REFRESH) } !is Error) {
startConsumingHints()
}
}
??通過上面的代碼档桃,我們可以看出來枪孩,實際上在進(jìn)行Refresh操作的時候,就已經(jīng)在監(jiān)聽加載更多操作藻肄,當(dāng)然這個實現(xiàn)我們也可以猜想得到的蔑舞,這里只不過驗證一下具體實現(xiàn)而已。我們來看startConsumingHints
:
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
private fun CoroutineScope.startConsumingHints() {
// 1. 嘗試觸發(fā)Refresh操作
if (config.jumpThreshold != COUNT_UNDEFINED) {
launch {
hintChannel.asFlow()
.filter { hint ->
hint.presentedItemsBefore * -1 > config.jumpThreshold ||
hint.presentedItemsAfter * -1 > config.jumpThreshold
}
.collectLatest { invalidate() }
}
}
// 2. 加載上一頁的數(shù)據(jù)
launch {
state.consumePrependGenerationIdAsFlow()
.collectAsGenerationalViewportHints(PREPEND)
}
// 3. 加載下一頁的數(shù)據(jù)
launch {
state.consumeAppendGenerationIdAsFlow()
.collectAsGenerationalViewportHints(APPEND)
}
}
??startConsumingHints
方法的實現(xiàn)很簡單嘹屯,但是這里有很多的細(xì)節(jié)需要注意:
- 這里有一個
jumpThreshold
攻询,關(guān)于這個變量的含義,我們之前已經(jīng)說過了州弟,具體就是當(dāng)前訪問的數(shù)據(jù)已經(jīng)超過了預(yù)先設(shè)置的閾值钧栖,那么直接就用Refresh操作來加載下一頁數(shù)據(jù)。這個會在RemoteMediator
里面會使用得到婆翔。- 通過調(diào)用
collectAsGenerationalViewportHints
實現(xiàn)了PREPEND
和APPEND
兩個操作拯杠。關(guān)于consumePrependGenerationIdAsFlow
和consumeAppendGenerationIdAsFlow
方法,我們這里就不講解了啃奴,因為這個涉及到maxSize
和DropEvent潭陪,所涉及的內(nèi)容就非常多,這里就不展開了最蕾。
??我們這里直接使用來看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)
}
??關(guān)于collectAsGenerationalViewportHints
方法依溯,內(nèi)部做了如下幾件事:
- 通過
flatMapLatest
操作流拍平。因為hintChannel.asFlow()
方法返回的一個Flow瘟则,所以需要拍平黎炉。其次,我們這里需要注意一下醋拧,如果generationId
為0拜隧,表示當(dāng)前沒有進(jìn)行Drop的操作,那么就不跳過第一個趁仙;如果不為0,那么進(jìn)行了Drop操作垦页,那么就跳過第一個雀费,因為在進(jìn)行Drop操作時,這里會收到一個generationId
痊焊,關(guān)于這個點盏袄,待會我單獨講解忿峻,這里先不展開。- 通過
conflate
操作符跳過之前發(fā)送的事件辕羽,保證只會取到一個事件逛尚。這個類似于RxJava里面的被壓問題,有興趣的同學(xué)可以看看這個操作符的原理刁愿,這里就不講解了绰寞。- 最后,就是調(diào)用
doLoad
方法數(shù)據(jù)請求铣口。
??說了這么多滤钱,拋開中間很多沒用的信息,其實從調(diào)用UiReceiver
的accessHint
方法開始脑题,最終會調(diào)用到doLoad
方法進(jìn)行網(wǎng)絡(luò)請求件缸。
??好了接下來,我們將重點分析doLoad
方法叔遂,此方法涉及的內(nèi)容非常的多他炊,大家要有心里準(zhǔn)備,不過我會盡最大的努力給大家解釋清楚已艰。
private suspend fun doLoad(
state: PageFetcherSnapshotState<Key, Value>,
loadType: LoadType,
generationalHint: GenerationalViewportHint
) {
// 1. 計算已經(jīng)加載的數(shù)量痊末。
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. 通過已經(jīng)加載的數(shù)量獲取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. 加載數(shù)據(jù)
val result: LoadResult<Key, Value> = pagingSource.load(params)
when (result) {
is Page<Key, Value> -> {
// ······
// 4. 插入數(shù)據(jù)
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設(shè)置為true舌胶,
// 表示當(dāng)前LoadType已經(jīng)沒有數(shù)據(jù)了。
if ((loadType == PREPEND && result.prevKey == null) ||
(loadType == APPEND && result.nextKey == null)
) {
endOfPaginationReached = true
}
}
// 省略Error的代碼疮丛。
}
val dropType = when (loadType) {
PREPEND -> APPEND
else -> PREPEND
}
// 6. 進(jìn)行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. 發(fā)送事件到UI層幔嫂。
pageEventCh.send(pageEvent)
}
// 省略RemoteMediator的代碼。
}
}
??通過上面的代碼誊薄,以及我添加的注釋履恩,我們可以知道,doLoad
方法一共做了4件事:
- 計算已經(jīng)加載數(shù)據(jù)的數(shù)量呢蔫,然后獲取對應(yīng)的key切心,用以后面的數(shù)據(jù)請求。在這里片吊,就用到了之前在創(chuàng)建
ViewportHint
所攜帶的信息绽昏。- 調(diào)用
PagingSource
的load方法,進(jìn)行數(shù)據(jù)請求俏脊。如果請求成功全谤,會通過PageFetcherSnapshotState
的insert
方法把對應(yīng)的數(shù)據(jù)插入進(jìn)去,這個操作我們在Refresh里面看到過了爷贫,這里就不講解了认然。- 進(jìn)行Drop操作补憾,嘗試丟棄失效的頁面。這一步非常的重要卷员,這個對于我們理解前面所說的
startConsumingHints
有很大的幫助盈匾。這里我先不講解它,后續(xù)會重點分析它毕骡。- 通過
pageEventCh
發(fā)送一個PageEvent
用來告知UI層削饵,數(shù)據(jù)已經(jīng)在加載完成。這個我們在前面分析過了挺峡,這里也不再講解了葵孤。
??總的來說,doLoad
方法的實現(xiàn)還是比較簡單的橱赠,當(dāng)然這里省略很多的細(xì)節(jié)尤仍,比如說Drop操作。不過狭姨,整個流程我們理解還是比較清晰宰啦,這里我對加載更多做一個簡單的總結(jié),方便大家來理解饼拍。
加載更多是一個UI層主動赡模,數(shù)據(jù)請求層被動的過程。UI層通過調(diào)用
UiReceiver
的accessHint
方法來告知數(shù)據(jù)請求層需要進(jìn)行加載更多的數(shù)據(jù)請求师抄,在調(diào)用的同時UI層通過傳遞一個ViewportHint
對象用來攜帶一些關(guān)鍵信息漓柑。數(shù)據(jù)請求層監(jiān)聽到這個行為,并且拿到ViewportHint
對象叨吮,通過一系列的計算獲取一個key辆布,進(jìn)而調(diào)用PagingSource
的load
方法進(jìn)行數(shù)據(jù)請求,數(shù)據(jù)請求完成之后茶鉴,進(jìn)行了一些常規(guī)操作之后锋玲,通過pageEventCh
發(fā)送一個PageEvent
用來告知UI層,數(shù)據(jù)已經(jīng)在加載完成涵叮。
??如上便是加載更多的整個過程惭蹂。接下來,為了大家理解更加的深刻割粮,我將對drop操作進(jìn)行分析盾碗,算是額外的福利內(nèi)容,因為內(nèi)容大綱并沒有寫這個舀瓢。
6. Drop操作
??前面在分析加載更多的時候置尔,反復(fù)的提到Drop操作,包括在介紹PageEvent時,也介紹Drop事件榜轿。那么到底什么是Drop,什么時候進(jìn)行Drop操作呢朵锣?
??一言以蔽之谬盐,Drop可以理解為裁剪,當(dāng)我們在創(chuàng)建PagingConfig
時诚些,有一個配置項--maxSize
飞傀,表示當(dāng)前數(shù)據(jù)總量。需要特別注意的是诬烹,這個maxSize
表示的意思并不是數(shù)據(jù)最大的數(shù)量砸烦,而是Adapter內(nèi)部的List可以存儲有效數(shù)據(jù)(非空數(shù)據(jù))的最大數(shù)據(jù)量。比如說绞吁,我們將maxSize 設(shè)置為200幢痘,那么表示Adapter內(nèi)部存儲只能200條,超過200條之后從頭開始丟棄家破⊙账担回到PagePresenter
里面來,這個類里面有三個變量用來統(tǒng)計數(shù)據(jù)總量汰聋,但是統(tǒng)計的數(shù)據(jù)是不同的门粪,如下:
名字 | 含義 |
---|---|
storageCount | 真實的數(shù)據(jù)總量,不包括為空的數(shù)據(jù)量烹困。 |
placeholdersBefore | 前置占位符的數(shù)量玄妈,這個范圍里面的數(shù)據(jù)獲取都為空。 |
placeholdersAfter | 后置占位符的數(shù)量髓梅。 |
??Adapter 在統(tǒng)計數(shù)據(jù)總量(getItemCount)時拟蜻,是通過PagePresenter
的size
方法來獲取的。即上述三個總量的和:
override val size: Int
get() = placeholdersBefore + storageCount + placeholdersAfter
??而PagingConfig
里面的maxSize
限制的是storageCount
女淑,這一點大家一定要清楚瞭郑。
??其次,maxSize
只在enablePlaceholders
為true生效鸭你,切記切記G拧!
??回到doLoad
方法袱巨,它之所以要在數(shù)據(jù)請求之后阁谆,且插入之后,進(jìn)行裁剪操作愉老,是為了讓maxSize 這個配置項生效场绿,不能讓總數(shù)據(jù)量超過設(shè)置的閾值。那么它是在怎么進(jìn)行裁剪的呢嫉入?主要分為兩步(下面代碼是doLoad方法部分代碼片段):
// 1. 通過dropEventOrNull方法計算需要裁剪的數(shù)據(jù)
state.dropEventOrNull(dropType, generationalHint.hint)?.let { event ->
// 2.裁剪數(shù)據(jù)
state.drop(event)
pageEventCh.send(event)
}
??這部分所做內(nèi)容的如下:
- 調(diào)用
dropEventOrNull
方法計算需要裁剪的數(shù)據(jù)焰盗,如果需要裁剪璧尸,那么會返回一個Drop
的PageEvent;如果不需要裁剪,那么就會返回為空熬拒。- 通過DropEvent裁剪數(shù)據(jù)爷光。首先是調(diào)用了
PageFetcherSnapshotState
的drop方法,將內(nèi)部存儲對應(yīng)的數(shù)據(jù)刪除澎粟;其次就發(fā)送一個PageEvent到UI層蛀序,告知UI層也要對應(yīng)的刪除。
??我們來看看PageFetcherSnapshotState
的drop方法的實現(xiàn):
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
里面對應(yīng)的數(shù)據(jù)。- 將appendLoadId++,并且通過
appendLoadIdCh
發(fā)送出去啸盏。
??那么哪里在消費appendLoadId
事件呢重贺?這就要回到加載更多的地方,當(dāng)時提到了consumeAppendGenerationIdAsFlow
方法宫补。是的檬姥,就是這個方法對外提供消費入口:
@OptIn(ExperimentalCoroutinesApi::class)
fun consumeAppendGenerationIdAsFlow(): Flow<Int> {
return appendLoadIdCh.consumeAsFlow()
.onStart { appendLoadIdCh.offer(appendLoadId) }
}
??從這里,我們便知道前面在collectAsGenerationalViewportHints
方法里面粉怕,為啥在generationId
(即generationId
)不為0時健民,需要丟棄一個數(shù)據(jù)了。不為0表示在進(jìn)行Drop贫贝,如果PREPEND
和APPEND
同時進(jìn)行加載秉犹,并且同時Drop,可能會導(dǎo)致死循環(huán)稚晚,所以需要跳過一個崇堵,讓任意一個加載成功,另一個加載失敗(因為會Drop)客燕。
??如上便是Drop的所有內(nèi)容鸳劳。
7. 總結(jié)
??到此,上篇的內(nèi)容到此結(jié)束也搓,在這里赏廓,我對本文內(nèi)容做一個簡單的總結(jié)。
- Paging3相比于Paging2傍妒,PagingSource(即Paging2的DataSource)Api簡潔了許多幔摸,使用起來也方便多了。
- 整個Paing3可以分為兩層颤练,分別是:數(shù)據(jù)請求層和Ui層既忆。兩個層之間通過Flow連接起來的。
- Refresh對于數(shù)據(jù)請求層來說,是一個主動的過程患雇,主要是通過
PageFetcherSnapshot
的doInitialLoad
方法進(jìn)行請求的跃脊。數(shù)據(jù)請求的基本過程如下:請求前,更新對應(yīng)LoadType的LoadState苛吱,并且同步到Ui層匾乓;其次,通過調(diào)用PagingSource
的load
方法獲取一個Load.Result對象又谋;然后隘道,根據(jù)Load.Result的類型進(jìn)行不同的操作登颓,如果是Load.Page對象移怯,主要是過程是神年,更新對應(yīng)LoadType的LoadState薪前,將數(shù)據(jù)添加到PageFetcherSnapshotState
里面褒翰,同時發(fā)送一個PageEvent到Ui層苍狰。- Append對于數(shù)據(jù)請求層來說是一個被動的過程露筒,由UI層觸發(fā)耻涛。主要是
UiReceiver
作為橋梁進(jìn)行請求废酷,最終會調(diào)用PageFetcherSnapshot
的doLoad
方法。請求的過程跟Refresh類似抹缕,只不過這個過程多了Drop操作澈蟆,Drop
主要是跟PagingConfig
里面的maxSize
有關(guān)。
??下篇我將分析RemoteMediator
卓研,敬請期待趴俘。