一、概述
在很久很久以前,加載并展示大量數(shù)據(jù)就已成為各家應(yīng)用中必不可少的業(yè)務(wù)場(chǎng)景,分頁(yè)加載也就成了必不可少的方案。在現(xiàn)有的Android API中也已存在支持分頁(yè)加載內(nèi)容的方案, 比如:
-
CursorAdapter:它簡(jiǎn)化了數(shù)據(jù)庫(kù)中數(shù)據(jù)到ListView中Item的映射, 僅查詢(xún)需要展示的數(shù)據(jù),但是查詢(xún)的過(guò)程是在UI線(xiàn)程中執(zhí)行。 - SupportV7包中的
AsyncListUtil支持基于position的數(shù)據(jù)集分頁(yè)加載到RecyclerView中,但不支持不基于position的數(shù)據(jù)集,而且它強(qiáng)制一個(gè)有限數(shù)據(jù)集中的null項(xiàng)必須展示Placeholder.
針對(duì)現(xiàn)有方案所存在的一些問(wèn)題,Google推出了Android架構(gòu)組件中的Paging Library, 不過(guò)目前還是alpha版本。Paging Library主要由3個(gè)部分組成:DataSource、PagedList、PagedListAdapter。
二、Paging Libray介紹
DataSource, PagedList, PagedAdapter三者之間的關(guān)系以及加載數(shù)據(jù)到展示數(shù)據(jù)的流程如下圖所示:

2.1 Datasource
顧名思義,Datasource<Key, Value>是數(shù)據(jù)源相關(guān)的類(lèi),其中Key對(duì)應(yīng)加載數(shù)據(jù)的條件信息,Value對(duì)應(yīng)返回結(jié)果, 針對(duì)不同場(chǎng)景,Paging提供了三種Datasource:
-
PageKeyedDataSource<Key, Value>:適用于目標(biāo)數(shù)據(jù)根據(jù)頁(yè)信息請(qǐng)求數(shù)據(jù)的場(chǎng)景,即Key字段是頁(yè)相關(guān)的信息。比如請(qǐng)求的數(shù)據(jù)的參數(shù)中包含類(lèi)似next/previous的信息。 -
ItemKeyedDataSource<Key, Value>:適用于目標(biāo)數(shù)據(jù)的加載依賴(lài)特定item的信息, 即Key字段包含的是Item中的信息,比如需要根據(jù)第N項(xiàng)的信息加載第N+1項(xiàng)的數(shù)據(jù),傳參中需要傳入第N項(xiàng)的ID時(shí),該場(chǎng)景多出現(xiàn)于論壇類(lèi)應(yīng)用評(píng)論信息的請(qǐng)求。 -
PositionalDataSource<T>:適用于目標(biāo)數(shù)據(jù)總數(shù)固定,通過(guò)特定的位置加載數(shù)據(jù),這里Key是Integer類(lèi)型的位置信息,T即Value。 比如從數(shù)據(jù)庫(kù)中的1200條開(kāi)始加在20條數(shù)據(jù)。
以上三種Datasource都是抽象類(lèi), 使用時(shí)需實(shí)現(xiàn)請(qǐng)求數(shù)據(jù)的方法。三種Datasource都需要實(shí)現(xiàn)loadInitial()方法, 各自都封裝了請(qǐng)求初始化數(shù)據(jù)的參數(shù)類(lèi)型LoadInitialParams。 不同的是分頁(yè)加載數(shù)據(jù)的方法,PageKeyedDataSource和ItemKeyedDataSource比較相似, 需要實(shí)現(xiàn)loadBefore()和loadAfter()方法,同樣對(duì)請(qǐng)求參數(shù)做了封裝,即LoadParams<Key>。PositionalDataSource需要實(shí)現(xiàn)loadRange(),參數(shù)的封裝類(lèi)為LoadRangeParams。
如果項(xiàng)目中使用Android架構(gòu)組件中的Room, Room可以創(chuàng)建一個(gè)產(chǎn)出PositionalDataSource的DataSource.Factory:
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);
總的來(lái)說(shuō),Datasource就像是一個(gè)抽水泵,而不是真正的水源,它負(fù)責(zé)從數(shù)據(jù)源加載數(shù)據(jù),可以看成是Paging Library與數(shù)據(jù)源之間的接口。
2.2 PagedList
如果將Datasource比作抽水泵,那PagedList就像是一個(gè)蓄水池,但不僅僅如此。PagedList是List的子類(lèi),支持所有List的操作, 除此之外它主要有五個(gè)成員:
mMainThreadExecutor: 一個(gè)主線(xiàn)程的Excutor, 用于將結(jié)果post到主線(xiàn)程。mBackgroundThreadExecutor: 后臺(tái)線(xiàn)程的Excutor.BoundaryCallback:加載Datasource中的數(shù)據(jù)加載到邊界時(shí)的回調(diào).-
Config: 配置PagedList從Datasource加載數(shù)據(jù)的方式, 其中包含以下屬性:-
pageSize:設(shè)置每頁(yè)加載的數(shù)量 -
prefetchDistance:預(yù)加載的數(shù)量 -
initialLoadSizeHint:初始化數(shù)據(jù)時(shí)加載的數(shù)量 -
enablePlaceholders:當(dāng)item為null是否使用PlaceHolder展示
-
PagedStorage<T>: 用于存儲(chǔ)加載到的數(shù)據(jù),它是真正的蓄水池所在,它包含一個(gè)ArrayList<List<T>>對(duì)象mPages,按頁(yè)存儲(chǔ)數(shù)據(jù)。
PagedList會(huì)從Datasource中加載數(shù)據(jù),更準(zhǔn)確的說(shuō)是通過(guò)Datasource加載數(shù)據(jù), 通過(guò)Config的配置,可以設(shè)置一次加載的數(shù)量以及預(yù)加載的數(shù)量。 除此之外,PagedList還可以向RecyclerView.Adapter發(fā)送更新的信號(hào),驅(qū)動(dòng)UI的刷新。
2.3 PagedListAdapter
PagedListAdapte是RecyclerView.Adapter的實(shí)現(xiàn),用于展示PagedList的數(shù)據(jù)。它本身實(shí)現(xiàn)的更多是Adapter的功能,但是它有一個(gè)小伙伴PagedListAdapterHelper<T>, PagedListAdapterHelper會(huì)負(fù)責(zé)監(jiān)聽(tīng)PagedList的更新, Item數(shù)量的統(tǒng)計(jì)等功能。這樣當(dāng)PagedList中新一頁(yè)的數(shù)據(jù)加載完成時(shí), PagedAdapte就會(huì)發(fā)出加載完成的信號(hào),通知RecyclerView刷新,這樣就省略了每次loading后手動(dòng)調(diào)一次notifyDataChanged().
除此之外,當(dāng)數(shù)據(jù)源變動(dòng)產(chǎn)生新的PagedList,PagedAdapter會(huì)在后臺(tái)線(xiàn)程中比較前后兩個(gè)PagedList的差異,然后調(diào)用notifyItem...()方法更新RecyclerView.這一過(guò)程依賴(lài)它的另一個(gè)小伙伴ListAdapterConfig, ListAdapterConfig負(fù)責(zé)主線(xiàn)程和后臺(tái)線(xiàn)程的調(diào)度以及DiffCallback的管理,DiffCallback的接口實(shí)現(xiàn)中定義比較的規(guī)則,比較的工作則是由PagedStorageDiffHelper來(lái)完成。
三、加載數(shù)據(jù)
使用Paging Library加載數(shù)據(jù)主要有兩種方式,一種是單一數(shù)據(jù)源的加載(本地?cái)?shù)據(jù)或網(wǎng)絡(luò)數(shù)據(jù)), 另一種是多個(gè)數(shù)據(jù)源的加載(本地?cái)?shù)據(jù)+網(wǎng)絡(luò)數(shù)據(jù))。
3.1 加載單一數(shù)據(jù)源的數(shù)據(jù)
首先我們可以通過(guò)LivePagedListBuilder來(lái)創(chuàng)建LiveData<PagedList>為UI層提供數(shù)據(jù)。整個(gè)流程如下圖所示:

如果數(shù)據(jù)源是DB,當(dāng)數(shù)據(jù)發(fā)生變化,DB會(huì)推送(push)一個(gè)新的PagedList(這里會(huì)依賴(lài)LiveData的機(jī)制). 如果是網(wǎng)絡(luò)數(shù)據(jù),即客戶(hù)端無(wú)法知道數(shù)據(jù)源的變化,可以通過(guò)諸如滑動(dòng)刷新的方式將調(diào)用Datasource的invalidate()方法來(lái)拉去(pull)新的數(shù)據(jù)。
3.2 加載多個(gè)數(shù)據(jù)源的數(shù)據(jù)
這種場(chǎng)景一般是先加載本地?cái)?shù)據(jù),加載完成后再加載網(wǎng)絡(luò)數(shù)據(jù),比較適合需要本地做緩存的業(yè)務(wù)。比如IM中的聊天消息,當(dāng)打開(kāi)聊天界面時(shí)先加載本地?cái)?shù)據(jù)庫(kù)中的聊天消息,加載完了再加載網(wǎng)絡(luò)的離線(xiàn)消息。這中場(chǎng)景的流程如下圖所示:

這種場(chǎng)景需要為PagedList設(shè)置BoundaryCallback來(lái)監(jiān)聽(tīng)加載完本地?cái)?shù)據(jù)的事件,觸發(fā)加載網(wǎng)絡(luò)數(shù)據(jù),然后入庫(kù),此時(shí)LiveData<PagedList>會(huì)推送一個(gè)新的PagedList, 并觸發(fā)界面刷新。
具體使用案例可以參考Google Sample的PagingWithNetworkSample項(xiàng)目。
四、小結(jié)
Paging Library作為Android架構(gòu)組件庫(kù)的一員,其特點(diǎn)主要還是在其架構(gòu)思想上。Paging將分頁(yè)的業(yè)務(wù)封裝為一條完整的流水線(xiàn),一個(gè)Pattern。其中各個(gè)組件之間存在聯(lián)動(dòng)的關(guān)系:
當(dāng)PagedList創(chuàng)建時(shí)會(huì)立即從
Datasource加載數(shù)據(jù)(觸發(fā)loadInitial()),DataSource加載到數(shù)據(jù)后會(huì)更新PagedList,PagedList更新會(huì)通知到PagedAdapter并刷新UI;UI上的展示會(huì)觸發(fā)
PagedAdapter的getItem()隨即觸發(fā)PagedList的loadAround()方法從DataSource加載周?chē)臄?shù)據(jù)...
整個(gè)過(guò)程Paging內(nèi)部實(shí)現(xiàn)了線(xiàn)程的切換,數(shù)據(jù)的預(yù)加載,所有聯(lián)動(dòng)的關(guān)系都內(nèi)聚到Paging中,這樣使用時(shí)只需要關(guān)心加載數(shù)據(jù)的具體實(shí)現(xiàn),并且在用戶(hù)體驗(yàn)上,將會(huì)大大減少等待數(shù)據(jù)加載的時(shí)間和次數(shù)。