來新公司半年多,最近一直在參與 Andorid 團(tuán)隊(duì)的架構(gòu)升級(jí)工作。最近在圖片選擇庫(kù)中使用了 paging 作為分頁(yè)加載框架。順便閱讀了一下paging的源碼。在這里記錄一下。
初次接除 paging, 可能會(huì)一臉懵逼,感覺出來了很多 API, 不知道從哪里下手。我們先對(duì) paging 的組成部分進(jìn)行一個(gè)了解。
首先,我們按照 列表分頁(yè)加載 這個(gè)行為進(jìn)行一個(gè)基本的劃分,分為 2 個(gè)部分, 數(shù)據(jù) 和 UI, paging 就是按照這個(gè)來進(jìn)行劃分的
數(shù)據(jù)
數(shù)據(jù)部分 paging 包括
-
PagedList一個(gè)繼承了AbstractList的List子類, 包括了數(shù)據(jù)源獲取的數(shù)據(jù) -
DataSource數(shù)據(jù)源的概念,分別提供了 PageKeyedDataSource、ItemKeyedDataSource、PositionalDataSource, 在數(shù)據(jù)源中,我們可以定義我們自己的數(shù)據(jù)加載邏輯。
UI
UI 部分 paging 提供了一個(gè)新的 PagedListAdapter, 在實(shí)例化這個(gè) Adapter 的時(shí)候,我們需要提供一個(gè)自己實(shí)現(xiàn)的 DiffUtil.ItemCallback 或者 AsyncDifferConfig
入門
以分頁(yè)數(shù)據(jù)源 PageKeyedDataSource 為例
創(chuàng)建一個(gè)數(shù)據(jù)源, 其中 Language 為 demo 中的實(shí)體對(duì)象
class LanguageDataSource: PageKeyedDataSource<Int, Language>()
實(shí)現(xiàn)三個(gè) override 方法
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Language>) {
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Language>) {
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Language>) {
}
著 3 個(gè)方法,依次解釋為
- 初次加載
- 后面一頁(yè)加載
- 前一頁(yè)加載
我們給第一頁(yè)數(shù)據(jù)填充邏輯
LanguageRepository.requestLanguages({datas->
if (datas.code == 200) {
val languages = datas.data
Handler(Looper.getMainLooper()).post {
callback.onResult(languages, null, 1)
}
} else {
}
}, {t->
Log.e(javaClass.simpleName, "${t.message}")
})
其中 LanguageRepository 是利用 retrofit 請(qǐng)求了一個(gè) Language 對(duì)象的列表。 我們調(diào)用
callback.onResult 就會(huì)刷新 RecyclerView 的視圖
loadAfter 的實(shí)現(xiàn)大致與 loadInitial 一致,這里不做贅述。
我們?cè)賮砜匆幌?UI 層,我們定義一個(gè) PagedListAdapter
class LanguageAdapter(private val context: Context) : PagedListAdapter<Language, ViewHolder>(languageDiff)
這里我們需要 override 2個(gè)方法
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
override fun onBindViewHolder(holder: ViewHolder, position: Int)
在 onBindViewHolder 中, 我們可以通過 getItem(position) 獲取相對(duì)于的數(shù)據(jù)實(shí)例去進(jìn)行 UI 的展示。
接下來是一個(gè)比較關(guān)鍵的部分,那就是如何連接 DATA 和 UI 這兩部分。
val config = PagedList.Config.Builder()
.setPageSize(15)
.setPrefetchDistance(2)
.setInitialLoadSizeHint(15)
.setEnablePlaceholders(false)
.build()
val pageList = PagedList.Builder(LanguageDataSource(), config)
.setNotifyExecutor {
Handler(Looper.getMainLooper()).post {it.run()}
}
.setFetchExecutor(Executors.newFixedThreadPool(2))
.build()
adapter.submitList(pageList)
在這里, pageList 的 NotifyExecutor 和 FetchExecutor 也是必須設(shè)置的。在 Android arch componet 完整的架構(gòu)中,更推薦使用構(gòu)建一個(gè) PageList 的 LiveData 的方式。但是不使用也沒有關(guān)系,arch compoent 的完整內(nèi)容在這里不做過多的描述。具體的詳細(xì)使用可以查看google的實(shí)例源碼
在大致了解了 paging 的組成部分后,我們會(huì)開始好奇,那我們到底為什么需要 paging 呢, 他和我們之前普通的使用方式有什么區(qū)別呢,我們可以在源碼中尋找到答案。
我們可以在 2 個(gè)部分的真正對(duì)接處作為切入點(diǎn)進(jìn)行分析,查看 PagedList.Builder#build() 的源碼:
return PagedList.create(
mDataSource,
mNotifyExecutor,
mFetchExecutor,
mBoundaryCallback,
mConfig,
mInitialKey);
繼續(xù)查看
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
跟到這個(gè)類的構(gòu)造方法,可以看到如下邏輯
mDataSource.dispatchLoadInitial(key,
mConfig.initialLoadSizeHint,
mConfig.pageSize,
mConfig.enablePlaceholders,
mMainThreadExecutor,
mReceiver);
這里以 PageKeyedDataSource 為例, 其他的 DataSource 對(duì)象同理
查看 dispatchLoadInital 方法
LoadInitialCallbackImpl<Key, Value> callback =
new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);
callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
這里我們可以看到, loadInitial 就是我們需要在 override 的方法之一。那我們里面調(diào)用 callback 的 onResult 方法到底發(fā)生了什么呢?
查看 LoadInitialCallbackImpl#onResult() 的源碼,關(guān)鍵邏輯如下
mDataSource.initKeys(previousPageKey, nextPageKey);
int trailingUnloadedCount = totalCount - position - data.size();
if (mCountingEnabled) {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
data, position, trailingUnloadedCount, 0));
} else {
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
查看 dispatchResultToReceiver
繼續(xù)查看 onPageResult 方法
我們關(guān)注一下 init 時(shí)候的邏輯
mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
pageResult.positionOffset, ContiguousPagedList.this);
init 的邏輯很簡(jiǎn)單,只有 2 行
init(leadingNulls, page, trailingNulls, positionOffset);
callback.onInitialized(size());
在這里, 我們可以看見關(guān)鍵的邏輯
mPages.clear();
mPages.add(page);
這里,和 PageList 綁定的數(shù)據(jù)就發(fā)生了變化。之后我們把 PageList submit 給了 adapter 那么,數(shù)據(jù)就發(fā)生了更新。
初始加載我們看完了,那么,剩下的數(shù)據(jù)是如何加載的呢
我們反過來看 RecyclerView, 如果我們滑動(dòng)列表或者其他操作的時(shí)候,很自然會(huì)調(diào)用 adapter 的 bind 方法。那么,我們?nèi)ゲ榭?PagedListAdapter#getItem 的源碼。
return mDiffer.getItem(position);
查看 PageList 的 loadAround
loadAroundInternal(index);
繼續(xù),
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
查看 scheduleAppend 的實(shí)現(xiàn)
mBackgroundThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (isDetached()) {
return;
}
if (mDataSource.isInvalid()) {
detach();
} else {
mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
}
});
這里,我們看到了 dispatchLoadAfter 方法的調(diào)用,之后的邏輯和之前的 dispathLoadInitial 就非常的類似了。
最終,會(huì)調(diào)用到如下邏輯
這里會(huì)走 AsyncPagedListDiffer 的 PagedList.Callback 的回調(diào)
這里,callback 是和 adapter 關(guān)聯(lián)起來的。所以會(huì)在這里刷新列表。
最后,我們看一下 Adapter 的 submit 方法,最后可以看到這樣的邏輯
我們可以看到 paging 是利用了 DiffUtils 對(duì) RecyclerView 進(jìn)行刷新的。這樣我們也無需擔(dān)心 paging 會(huì)存在性能問題。
理解
最后談一下對(duì) paging 的理解。 一般情況下,我們最原始的方式,列表 UI 所在的部分,是需要知道數(shù)據(jù)的來源等邏輯部分,我們?cè)诔R姷?mvp 模式中,會(huì)對(duì)數(shù)據(jù)和 UI 進(jìn)行分層。 而 paging 就利用一系列的封裝, 提供了更加通用的 API 調(diào)用來做這些事情。更通俗點(diǎn)說,就是實(shí)現(xiàn)了分頁(yè)加載結(jié)構(gòu)中的 Presenter 層及 Presenter層的下游處理部分。
這種模式,業(yè)務(wù)的編寫者,可以把 UI 部分的代碼模板化, 只需要關(guān)心業(yè)務(wù)邏輯,并且把業(yè)務(wù)邏輯中的數(shù)據(jù)獲取寫在 DataSource 中,使分頁(yè)加載的操作解耦程度更高。