一控硼、概述
在很久很久以前泽论,加載并展示大量數(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的映射, 僅查詢需要展示的數(shù)據(jù)幔妨,但是查詢的過(guò)程是在UI線程中執(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è)主線程的Excutor, 用于將結(jié)果post到主線程。mBackgroundThreadExecutor
: 后臺(tái)線程的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)線程中比較前后兩個(gè)PagedList的差異疾捍,然后調(diào)用notifyItem...()方法更新RecyclerView.這一過(guò)程依賴(lài)它的另一個(gè)小伙伴ListAdapterConfig
奈辰, ListAdapterConfig負(fù)責(zé)主線程和后臺(tái)線程的調(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ù)岩榆,即客戶端無(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ò)的離線消息。這中場(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ù)封裝為一條完整的流水線,一個(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)了線程的切換谨敛,數(shù)據(jù)的預(yù)加載,所有聯(lián)動(dòng)的關(guān)系都內(nèi)聚到Paging中滤否,這樣使用時(shí)只需要關(guān)心加載數(shù)據(jù)的具體實(shí)現(xiàn)脸狸,并且在用戶體驗(yàn)上,將會(huì)大大減少等待數(shù)據(jù)加載的時(shí)間和次數(shù)藐俺。