深入理解 Android paging 分頁載入庫
來新公司半年多,最近一直在參與 Andorid 團隊的架構升級工作。最近在圖片選擇庫中使用了 paging 作為分頁載入框架。順便閱讀了一下paging的原始碼。在這裡記錄一下。
初次接除 paging, 可能會一臉懵逼,感覺出來了很多 API, 不知道從哪裡下手。我們先對 paging 的組成部分進行一個瞭解。
首先,我們按照 列表分頁載入
這個行為進行一個基本的劃分,分為 2 個部分, 資料
和 UI
, paging 就是按照這個來進行劃分的
資料
資料部分 paging 包括
-
PagedList
一個繼承了AbstractList
的List
子類, 包括了資料來源獲取的資料 -
DataSource
資料來源的概念,分別提供了 ofollow,noindex" target="_blank">PageKeyedDataSource 、 ItemKeyedDataSource 、 PositionalDataSource , 在資料來源中,我們可以定義我們自己的資料載入邏輯。
UI
UI 部分 paging 提供了一個新的 PagedListAdapter
, 在例項化這個 Adapter
的時候,我們需要提供一個自己實現的 DiffUtil.ItemCallback
或者 AsyncDifferConfig
入門
以分頁資料來源 PageKeyedDataSource
為例
建立一個數據源, 其中 Language 為 demo 中的實體物件
class LanguageDataSource: PageKeyedDataSource<Int, Language>()
實現三個 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 個方法,依次解釋為
- 初次載入
- 後面一頁載入
- 前一頁載入
我們給第一頁資料填充邏輯
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
請求了一個 Language 物件的列表。 我們呼叫 callback.onResult
就會重新整理 RecyclerView 的檢視
loadAfter
的實現大致與 loadInitial
一致,這裡不做贅述。
我們再來看一下 UI 層,我們定義一個 PagedListAdapter
class LanguageAdapter(private val context: Context) : PagedListAdapter<Language, ViewHolder>(languageDiff)
這裡我們需要 override 2個方法
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
override fun onBindViewHolder(holder: ViewHolder, position: Int)
在 onBindViewHolder
中, 我們可以通過 getItem(position)
獲取相對於的資料例項去進行 UI 的展示。
接下來是一個比較關鍵的部分,那就是如何連線 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
也是必須設定的。在 Android arch componet
完整的架構中,更推薦使用構建一個 PageList
的 LiveData
的方式。但是不使用也沒有關係, arch compoent
的完整內容在這裡不做過多的描述。具體的詳細使用可以檢視 google的例項原始碼
在大致瞭解了 paging 的組成部分後,我們會開始好奇,那我們到底為什麼需要 paging 呢, 他和我們之前普通的使用方式有什麼區別呢,我們可以在原始碼中尋找到答案。
我們可以在 2 個部分的真正對接處作為切入點進行分析,檢視 PagedList.Builder#build()
的原始碼:
return PagedList.create( mDataSource, mNotifyExecutor, mFetchExecutor, mBoundaryCallback, mConfig, mInitialKey);
繼續檢視
return new ContiguousPagedList<>(contigDataSource, notifyExecutor, fetchExecutor, boundaryCallback, config, key, lastLoad);
跟到這個類的構造方法,可以看到如下邏輯
mDataSource.dispatchLoadInitial(key, mConfig.initialLoadSizeHint, mConfig.pageSize, mConfig.enablePlaceholders, mMainThreadExecutor, mReceiver);
這裡以 PageKeyedDataSource
為例, 其他的 DataSource
物件同理
檢視 dispatchLoadInital
方法
LoadInitialCallbackImpl<Key, Value> callback = new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver); loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback); callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
這裡我們可以看到, loadInitial
就是我們需要在 override 的方法之一。那我們裡面呼叫 callback 的 onResult 方法到底發生了什麼呢?
檢視 LoadInitialCallbackImpl#onResult()
的原始碼,關鍵邏輯如下
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
繼續檢視 onPageResult
方法
我們關注一下 init
時候的邏輯
mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls, pageResult.positionOffset, ContiguousPagedList.this);
init
的邏輯很簡單,只有 2 行
init(leadingNulls, page, trailingNulls, positionOffset); callback.onInitialized(size());
在這裡, 我們可以看見關鍵的邏輯
mPages.clear(); mPages.add(page);
這裡,和 PageList
繫結的資料就發生了變化。之後我們把 PageList
submit 給了 adapter
那麼,資料就發生了更新。
初始載入我們看完了,那麼,剩下的資料是如何載入的呢
我們反過來看 RecyclerView
, 如果我們滑動列表或者其他操作的時候,很自然會呼叫 adapter 的 bind 方法。那麼,我們去檢視 PagedListAdapter#getItem
的原始碼。
return mDiffer.getItem(position);
檢視 PageList
的 loadAround
loadAroundInternal(index);
繼續,
if (mAppendItemsRequested > 0) { scheduleAppend(); }
檢視 scheduleAppend
的實現
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
方法的呼叫,之後的邏輯和之前的 dispathLoadInitial
就非常的類似了。
最終,會呼叫到如下邏輯
這裡會走 AsyncPagedListDiffer
的 PagedList.Callback
的回撥
這裡,callback 是和 adapter 關聯起來的。所以會在這裡重新整理列表。
最後,我們看一下 Adapter
的 submit 方法,最後可以看到這樣的邏輯
我們可以看到 paging 是利用了 DiffUtils
對 RecyclerView 進行重新整理的。這樣我們也無需擔心 paging 會存在效能問題。
理解
最後談一下對 paging 的理解。 一般情況下,我們最原始的方式,列表 UI 所在的部分,是需要知道資料的來源等邏輯部分,我們在常見的 mvp 模式中,會對資料和 UI 進行分層。 而 paging 就利用一系列的封裝, 提供了更加通用的 API 呼叫來做這些事情。更通俗點說,就是實現了分頁載入結構中的 Presenter 層及 Presenter層的下游處理部分。
這種模式,業務的編寫者,可以把 UI 部分的程式碼模板化, 只需要關心業務邏輯,並且把業務邏輯中的資料獲取寫在 DataSource 中,使分頁載入的操作解耦程度更高。