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 ,表示當前加載完畢頁之後需要加載佔位符的數量。 |
- 在加載數據完成之後返回
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。 |
- 我們將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<Data>>結構, 而這個 pageOffset 表示的時List<Data>的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
,敬請期待。