距離上一篇Jetpack源碼分析的文章已經兩個月,時間間隔確實有點長。最近,感覺自己的學習積極性不那麼的高,看Paging的源碼也是斷斷續續的。時至今日,纔算是完成對Paging的源碼學習。今天我們就來學習Paging的實現原理。
本文參考資料:
注意,本文Paging相關源碼均來自於2.1.2版本。
1. 概述
在日常開發中,我們經常能接觸得到一個場景--需要加載列表數據,通常來說,列表數據的顯示可以用RecyclerView,但是列表數據的加載並沒有現成的庫或者工具類供我們使用。從另一個方面來說,對於大量的列表數據,我們不可能一次將它一次性從後臺獲取過來,所以分頁加載是必要的事。
由於Google爸爸沒有給我們提供現成的輪子,所以在此之前,我們需要分頁加載,都是自己簡單的實現。通常實現方案是:通過RecyclerView對OnScrollerListener
的onScrolled方法的回調,我們可以在這個方法裏面監聽並且計算位置,當符合加載時機時,就可以加載下一頁的數據。
上面的實現方案是無可厚非的,並且還比較簡單,實現起來也比較容易。但是有啥問題呢?我們從下面幾個方面來看看:
- 耦合度比較高。OnScrollerListener在計算位置的時候,通常來說會依賴RecyclerView的LayoutManager,不同LayoutManager有不同計算方式,如果後面RecyclerView有很多不同的LayoutManager,OnScrollerListener裏面就會變的非常複雜。
- 擴展性比較差。這個可以從兩個方面來介紹:首先,在不同的業務場景中,加載下一頁的方式可能不一樣,可能是通過Position獲取下一頁,也有可能是通過key獲取下一次,針對於此類情形,OnScrollerListener必須單獨處理;其次,通常來說,RecyclerView不僅僅有加載下一頁數據的場景,也有可能加載上一頁的場景的,針對於此類情形,OnScrollerListener也需要單獨處理。
其實,理想的情況是每種業務場景互相互相獨立,而不是糅合在一個類裏面。當然,這些問題其實可有可無,因爲經過簡單的拆分和整理,還是可以完全避免。而我們今天介紹的Paging
,是Google爸爸爲了解決分頁加載的問題而推出的一個庫。出於偷懶的原則,既然Google爸爸已經爲我們實現了,我們爲啥還要自己搞呢,對吧?
針對於Paging,我也不過多的介紹它是啥,它是怎麼使用,相信大家非常的熟悉。我們就直接進入本文的主題--從源碼角度來學習一下Paging的實現原理。
2. 基本架構
Paging雖然是Jetpack成員中的一份子,但是卻跟其他成員(Lifecycle、ViewModel等)不一樣。其他的成員可能就是幾個類就能搞定實現,但是Paging卻不一樣,裏面涉及的類特別的多,所以在正式分析它的源碼之前,我們先來看一下它的架構實現。同時,我們從這裏可以看出來,Google爸爸對Paging的期望很高,否則爲啥會不遺餘力的設計和實現它。
從實現上來看,Paging
主要分爲3個部分:PagedListAdapter
、PagedList
和DataSource
。這其中,PagedListAdapter
和DataSource
比較熟悉,因爲我們在使用過程中必須自定義它倆,相對而言,我們對PagedList
要陌生一些。不管怎麼樣,我們都先來了解一下它們。
(1). PagedListAdapter
從本質上來說,PagedListAdapter
其實就是RecyclerView中 的Adapter的實現類,本身承載的作用就是Adapter本身的作用。不過,相比於其他的Adapter,PagedListAdapter
的內部卻也有些不同。
PagedListAdapter
內部有一個AsyncPagedListDiffer
類,這個類接管了Adapter對數據源的所有操作,其中包括:
- submitList:該方法的作用就是給Adapter設置一個新的數據源,由於Adapter可能存在的舊數據源,所以需要使用DiffUtil來進行差量計算。
AsyncPagedListDiffer
將這個方法的具體操作接管了過去,其實內部就是進行差量計算。我們通過這個方法的參數,還可以注意到一個小細節,就是該方法的參數是一個PagedList
。進而可以知道,AsyncPagedListDiffer
內部的維護PagedList對象。- getItem:該方法的作用是從數據源中獲取對應位置的Data數據。
AsyncPagedListDiffer
將其也接管過去了,其內部實現其實就是從PagedList
裏面獲取,PagedList
的本質就是一個List。需要特別注意的是,該方法的數據可能會爲空,所以一定要做防空的保護,具體爲啥會爲空呢?待會我們分析在PagedList會重點介紹。- getItemCount:該方法的作用是返回數據源的總個數。同
getItem
方法,該方法也被AsyncPagedListDiffer
接管過去了。
總的來說,AsyncPagedListDiffer
接管了Adapter對數據源的操作,同時在這個過程中還承擔了一個角色:作爲PagedList操作Adapter的中間橋樑。
可能有人會問,什麼是PagedList操作Adapter?我們知道,當數據源發生了改變,比如說進行了add、remove或者update的操作,要想操作生效,必須調用對應的notifyXXX方法。AsyncPagedListDiffer
在初始化PagedList時,會向其中註冊一個回調接口,用來監聽這一部分的操作,當回調產生,會調用Adapter對應的方法。這個待會我們在分析源碼,可以簡單的從源碼角度看一下。
(2). PagedList
PagedList
相較於PagedListAdapter
來說,要稍微複雜。我們主要從兩個方面看一下PagedList:
PagedList本身是基類,提供很多通用的方法,比如說
size
方法、getLastKey
方法等。這些方法每個子類的實現都差不多,但是isContiguous
方法就不一樣,它可以將PagedList分爲兩個部分:連續的還是非連續的。那麼我們怎麼來理解這連續的概念呢?我們知道數據都是通過分頁加載的方式,連續的數據,我們理解爲下一頁的數據跟上一頁的數據有一定的關係,比如說下一頁的數據是通過上一頁某一個key獲取的得來;非連續的數據,我們可以連接爲下一頁的數據跟上一頁的數據沒有關係,比如說PositionalDataSource
是完全通過position來獲取數據,當然從一定意義來說,連續性的數據和非連續性的數據沒有本質的區別,這個我們在後面可以看到。
通過isContiguous
方法劃分,我們大致可以將PageList分爲兩類:
從上面的uml類圖中,我們知道連續的PagedList對應的實現類是ContiguousPagedList
,非連續的PagedList
對應的實現類是TiledPagedList
。從uml類圖,我們還可以得到一個信息就是,就是這兩個個部分的PagedList關心的重點是不一樣的:
- ContiguousPagedList關心的是
onPagePrepended
和onPageAppended
,也就是說,連續的PagedList關心的是上一頁數據和下一頁數據的加載。同時我們從源碼可以簡單的看到,類似於onPageInserted
這類TiledPagedList
比較關心的方法,在ContiguousPagedList
的內部是不支持的。- TiledPagedList關心的是
onPageInserted
方法,也就是說,非連續的PagedList關心的是數據的插入,這裏我們將其理解爲下一頁數據的加載。同理,ContiguousPagedList
關心的方法在TiledPagedList
的內部也是不支持的。
PagedList還有一個簡單的實現類--SnapshotPagedList
,該類的實現比較簡單,且用途單一,本文就不討論了(不知道Google爸爸實現這個類幹嘛用的,很雞肋)。
PagedList從本質來說,就是一個List接口的實現類,跟ArrayList差不多,其實就是集合,所以Adapter通過它來獲取對應的Data,也是不無道理的。與ArrayList不同的是,PagedList
還負責加載數據的功能(實則不是PagedList來加載,而是通過PagedList通知dataSource來加載數據。)。
(3). DatDataSource
要說這三兄弟中最複雜的部分非DataSource莫屬,DatDataSource
複雜點主要是體現如下兩個方面:
- DataSource的實現類比較多。跟PagedList比較類似,DataSource也可以非分爲連續的和非連續的;但是跟PagedList不一樣,每個部分的實現類均還有實現類(主要分頁加載的場景比較多。)。
- DataSource承擔的功能比較複雜。顧名思義,我們從
DataSource
的名字,就知道它的作用是產生和維護數據。
我們先來簡單的看一下DatDataSource的uml類圖:
跟PagedList類似,我們可以從上面的uml類圖發現,連續的和非連續的DataSource的重點是不一樣的,這裏就不反覆介紹了。
(4).三兄弟的關係
上面分別介紹了一下三兄弟的各自作用,在這裏,我們簡單的看一下這三兄弟的關係,即它們三兄弟是怎麼聯繫來的。
- PagedListAdapter:直接面對RecyclerView,只是要從PagedList裏面獲取對應的Data。同時,加載下一頁數據的時機也是由它觸發的,Adapter通過
getItem
方法從PagedList中獲取數據的同時,還通過調用PagedList的loadAround
方法觸發加載下一頁數據的時機。- PagedList:首先是給
PagedListAdapter
提供對應的接口,讓其能夠獲取數據以及加載下一頁的數據;其次就是,直接持有DataSource的引用,可以直接對其進行對應的操作,比如說,加載數據等。- DataSource:三兄弟中最底層和最累的一個,主要是對
PagedList
提供接口,讓其能夠進行對應的操作。
到這裏,我們對Paging庫裏面基本組成部分有了一個大概的瞭解,接下來我們將從源碼角度來分析一下Paging的主要實現原理,本文主要從如下幾個方面來分析Paging:
- paging如何進行初始化第一頁數據(類似於刷新)。
- paging如何加載下一頁的數據。
- 從源碼角度來分析 PagedList的Config配置。
3.數據的加載
我們都知道paging是用來進行分頁加載的,所謂分頁加載,重點當然在加載,進一步的細化,我們需要了解的是:paging是怎麼初始化數據,以及怎麼加載下一頁數據的。這裏,我們分開來看這個方面,至於paging的基本使用,本文就不介紹了,不熟悉的同學可以參考 Android Jetpack- paging的基本使用這篇文章。
(1).加載第一頁數據。
通常來說,加載第一頁數據的方式不僅是第一次加載數據,還有一種方式就是通過刷新加載數據,此種方式會使之前的PagedList完全,進而重新創建一個新的PagedList對象來存儲數據。
雖然說加載的方式有兩種,但是從源碼角度來看,其實都是一樣的,接下來我們看一下對應的源碼。
通常來說,我們使用Paging,都是在ViewModel裏面創建一個LiveData<PagedList>對象,我們就從這個點開始分析源碼。我們可以通過如下的方式創建LiveData<PagedList>對象:
val mPageListLiveData = LivePagedListBuilder(mFactory, PagedList.Config.Builder().apply {
setPageSize(20)
setEnablePlaceholders(true)
}.build()).build()
LiveData<PagedList>對象是通過LivePagedListBuilder
的build方法創建的,這其中LivePagedListBuilder
的構造方法,第一個參數是DataSource.Factory
,該工廠類的作用用來創建DataSource對象,所以我們使用Paging
的步驟中,一個必不可少的步驟就是創建對應的DataSource的工廠類;第二參數就是創建PagedList.Config對象,主要的作用是設置分頁加載基本參數,比如說每頁加載大的大小以及預取下一頁的距離等。
假設我們正確的配置了分頁加載的基本參數(我們這裏強調了正確的配置,顧名思義也有錯誤的配置,這個我們在後面分析Config會重點介紹。),最後就是調用LivePagedListBuilder
的build方法創建LiveData<PagedList>對象。我們來看看build方法的實現:
@NonNull
@SuppressLint("RestrictedApi")
public LiveData<PagedList<Value>> build() {
return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
ArchTaskExecutor.getMainThreadExecutor(), mFetchExecutor);
}
build
方法本身沒有做什麼事,直接調用了create方法,我們看一下create方法實現:
@AnyThread
@NonNull
@SuppressLint("RestrictedApi")
private static <Key, Value> LiveData<PagedList<Value>> create(
@Nullable final Key initialLoadKey,
@NonNull final PagedList.Config config,
@Nullable final PagedList.BoundaryCallback boundaryCallback,
@NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull final Executor notifyExecutor,
@NonNull final Executor fetchExecutor) {
return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
@Nullable
private PagedList<Value> mList;
@Nullable
private DataSource<Key, Value> mDataSource;
private final DataSource.InvalidatedCallback mCallback =
new DataSource.InvalidatedCallback() {
@Override
public void onInvalidated() {
invalidate();
}
};
@SuppressWarnings("unchecked") // for casting getLastKey to Key
@Override
protected PagedList<Value> compute() {
// ·······
}
}.getLiveData();
}
create
方法裏面看似代碼非常多且複雜,實際上就是創建ComputableLiveData
對象,然後獲取了ComputableLiveData
裏面的LiveData。
從名字上來看,我們都以爲ComputableLiveData
是LiveData的實現類,實際上不是的;ComputableLiveData
可以理解爲LiveData
的包裝類。那麼ComputableLiveData
裏面都封裝了啥玩意呢?我們可以簡單的看一下ComputableLiveData
的源碼:
@VisibleForTesting
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
boolean computed;
do {
computed = false;
// compute can happen only in 1 thread but no reason to lock others.
if (mComputing.compareAndSet(false, true)) {
// as long as it is invalid, keep computing.
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
value = compute();
}
if (computed) {
mLiveData.postValue(value);
}
} finally {
// release compute lock
mComputing.set(false);
}
}
// check invalid after releasing compute lock to avoid the following scenario.
// Thread A runs compute()
// Thread A checks invalid, it is false
// Main thread sets invalid to true
// Thread B runs, fails to acquire compute lock and skips
// Thread A releases compute lock
// We've left invalid in set state. The check below recovers.
} while (computed && mInvalid.get());
}
};
// invalidation check always happens on the main thread
@VisibleForTesting
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = mLiveData.hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
mExecutor.execute(mRefreshRunnable);
}
}
}
};
簡單來說,ComputableLiveData
的核心就是兩個Runnable:mInvalidationRunnable
和mRefreshRunnable
。
- mInvalidationRunnable:通過調用
ComputableLiveData
的invalidate
方法會執行這個Runnable。這個Runnable內部本身沒有承載很多的功能,就是簡單的判斷了一下狀態,然後執行mRefreshRunnable
來刷新數據。mInvalidationRunnable
存在的意義就是爲我們提供刷新的操作,比如說我們通過下拉刷新想要刷新當前的數據,應該怎麼怎麼實現呢?我們都是通過調用DataSource的invalidate
方法來實現,而DataSource的invalidate
方法就會回調到ComputableLiveData
的invalidate
方法,進而實現刷新邏輯。至於爲啥如此回調,大家可以看一下上面create方法中的InvalidatedCallback
的實現。- mRefreshRunnable:
mRefreshRunnable
的實現比mInvalidationRunnable
比較複雜一點,但是不管怎麼複雜,實際就是調用compute
方法創建一個PagedList對象。
我們來compute方法的實現,看看它是怎麼創建PagedList對象的:
protected PagedList<Value> compute() {
@Nullable Key initializeKey = initialLoadKey;
if (mList != null) {
initializeKey = (Key) mList.getLastKey();
}
do {
if (mDataSource != null) {
mDataSource.removeInvalidatedCallback(mCallback);
}
// 創建DataSource對象。
mDataSource = dataSourceFactory.create();
mDataSource.addInvalidatedCallback(mCallback);
// 創建PagedList。
mList = new PagedList.Builder<>(mDataSource, config)
.setNotifyExecutor(notifyExecutor)
.setFetchExecutor(fetchExecutor)
.setBoundaryCallback(boundaryCallback)
.setInitialKey(initializeKey)
.build();
} while (mList.isDetached());
return mList;
}
我可以compute
方法的實現分爲兩步:
- 創建DataSource對象。在這裏,我們可以看到調用
DataSource.Factory
的create
方法創建了DataSource;同時,從這裏,我們可以知道每次刷新,DataSource對象都會重新創建,所以大家在使用Paging時,千萬不要嘗試在DataSource.Factory
裏面複用DataSource
對象。- 通過
PagedList.Builder
創建一個PagedList對象。
到這裏,我們並沒有看到調用數據加載的方法。我們進一步往下看,看一下PagedList.Builder
的build方法:
@WorkerThread
@NonNull
public PagedList<Value> build() {
// TODO: define defaults, once they can be used in module without android dependency
if (mNotifyExecutor == null) {
throw new IllegalArgumentException("MainThreadExecutor required");
}
if (mFetchExecutor == null) {
throw new IllegalArgumentException("BackgroundThreadExecutor required");
}
//noinspection unchecked
return PagedList.create(
mDataSource,
mNotifyExecutor,
mFetchExecutor,
mBoundaryCallback,
mConfig,
mInitialKey);
}
build
方法並沒有做啥事,只是調用了PagedList
的create方法,我們來看看create方法的實現(不得不吐槽,這調用棧太深了...):
static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key) {
if (dataSource.isContiguous() || !config.enablePlaceholders) {
int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
if (!dataSource.isContiguous()) {
//noinspection unchecked
dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
.wrapAsContiguousWithoutPlaceholders();
if (key != null) {
lastLoad = (Integer) key;
}
}
ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
} else {
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
(key != null) ? (Integer) key : 0);
}
}
在create方法裏面,我們可以發現,這裏通過一定的條件來判斷是創建ContiguousPagedList
還是TiledPagedList
。這個條件主要從兩個方面考慮:
- DataSource是否支持連續的數據,通過
isContiguous
方法來判斷。通過上面的內容,我們知道ItemKeyedDataSource
和PageKeyedDataSource
都是連續的。- 如果config裏面配置不支持佔位符,表示DataSource支持連續的數據。如果DataSource本身不支持連續的數據,那麼就通過
wrapAsContiguousWithoutPlaceholders
方法將DataSource轉換成支持連續性數據的DataSource。也就是說,如果我們使用的是PositionalDataSource
,但是在config配置了不支持佔位符,那麼就DataSource轉換爲支持連續性數據的DataSource。
create方法的實現主要涉及到上面的兩點,看上去實現沒有啥問題,但是我不得不吐槽一下:
- create方法是在
PagedList
裏面。PagedList
作爲父類,還要關心子類的實現,這個設計我覺得有待商榷的,這裏完全可以使用工廠模式或者建造者模式來創建對象,而不是在父類裏面創建子類對象。- 如果config裏面配置了不支持佔位符,就將DataSource變爲連續性的。這個坑,我相信大家都多多少少的躺過,我不得不吐槽,爲啥要這樣的設計。對外的實現不透明固然是好的,但是這裏總感覺是爲了實現佔位符的功能,而挖了大坑。在這種情況下,非連續的DataSource不支持佔位符完全可以拋異常,而不是兼容...不知道Google爸爸是怎麼想的。
吐槽歸吐槽,我們還是繼續的看一下兩個PagedList構造方法的實現,先來看看ContiguousPagedList
:
ContiguousPagedList(
@NonNull ContiguousDataSource<K, V> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<V> boundaryCallback,
@NonNull Config config,
final @Nullable K key,
int lastLoad) {
super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
boundaryCallback, config);
mDataSource = dataSource;
mLastLoad = lastLoad;
if (mDataSource.isInvalid()) {
detach();
} else {
mDataSource.dispatchLoadInitial(key,
mConfig.initialLoadSizeHint,
mConfig.pageSize,
mConfig.enablePlaceholders,
mMainThreadExecutor,
mReceiver);
}
mShouldTrim = mDataSource.supportsPageDropping()
&& mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
}
其他地方我們不用關心,我們可以看到在這裏調用了DataSource
的dispatchLoadInitial
方法,這個方法就是用來請求第一頁的數據。我們來看看它的實現,這裏以ItemKeyedDataSource
爲例:
@Override
final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
LoadInitialCallbackImpl<Value> callback =
new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
// If initialLoad's callback is not called within the body, we force any following calls
// to post to the UI thread. This constructor may be run on a background thread, but
// after constructor, mutation must happen on UI thread.
callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
}
dispatchLoadInitial
方法做的是比較簡單,就是創建了一個LoadInitialCallbackImpl
,然後就是調用loadInitial
方法進行請求數據,這個方法也是我們在自定義DataSource時必須重寫和實現的方法。
在這裏,最最關鍵的一個一步就是調用setPostExecutor
方法設置mainThreadExecutor
。有人疑惑這個到底有啥用?這個就得從loadInitial
本身的實現說起,相信大家都有一個疑惑,就是我們在這方法執行網絡到底應該放在子線程,還是保持該方法的原線程呢?從Okhttp層面上來說,我們是應該直接調用execute
還是enqueue
方法呢?
從這個方法本身的註解來看,我們直接通過execute
就行了,因爲該方法的執行本身就放在子線程裏面的:
@WorkerThread
public abstract void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<T> callback);
而我想說的是,其實兩種方式都是可以,就是因爲調用了setPostExecutor
方法。從兩個方面來分析一下這個問題:
- 不切換線程。如果我們不切換線程,那麼
loadInitial
方法就是阻塞型,必須等網絡請求完成之後,才能保證PagedList創建成功。也就是說,PagedListAdapter的submitList方法會等待到網絡請求才會回調,同時保證了提交的PagedList是肯定有數據的。- 切換線程。
loadInitial
方法就不是阻塞型的,那麼肯定在網絡請求完成之前,setPostExecutor
會被調用,那麼請求會的數據也會通過mainThreadExecutor
對象post到主線程,從而保證Adapter的notifyXXX方法在主線程被調用。這種情況,需要特別注意的是submitList
方法被回調時,提交的PagedList是一個空數據的數組。
我記得在Google的Demo--PagingWithNetworkSample(現在是paging3了)裏面,既有子線程調用的樣例,也有主線程的樣例,其實都是可以的。對此,大家不用再存疑。
我們自定義loadInitial
方法,會將請求完成的結果通過callback
的onResult方法回調過來,比如說,如下的代碼:
@WorkerThread
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<Message>
) {
val execute = getService().getMessage(params.pageSize, 0).execute()
val messageList = execute.body()
val errorBody = execute.errorBody()
if (execute.code() == 200 && messageList != null && errorBody == null) {
callback.onResult(messageList, 0, Int.MAX_VALUE)
} else {
callback.onResult(Collections.emptyList(), 0)
}
}
那麼爲什麼必須要調用onResult
方法呢?onResult
方法裏面到底做什麼啥事呢?今天我們看一下LoadInitialCallbackImpl
的onResult
方法的實現:
public void onResult(@NonNull List<T> data, int position) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
if (position < 0) {
throw new IllegalArgumentException("Position must be non-negative");
}
if (data.isEmpty() && position != 0) {
throw new IllegalArgumentException(
"Initial result cannot be empty if items are present in data set.");
}
if (mCountingEnabled) {
throw new IllegalStateException("Placeholders requested, but totalCount not"
+ " provided. Please call the three-parameter onResult method, or"
+ " disable placeholders in the PagedList.Config");
}
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
LoadInitialCallbackImpl
存在兩個onResult
方法,其中如果在Config中開啓了執行佔位符,最好是調用帶totalCount
的onResult
;反之,則調用另一個onResult。我相信,大家對此也有疑問,本文在後面介紹Config的配置時,會重點介紹,這裏就先不贅述。
回調最終會走到LoadCallbackHelper
的dispatchResultToReceiver
方法裏面,我們來看看:
void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
Executor executor;
synchronized (mSignalLock) {
if (mHasSignalled) {
throw new IllegalStateException(
"callback.onResult already called, cannot call again.");
}
mHasSignalled = true;
executor = mPostExecutor;
}
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
mReceiver.onPageResult(mResultType, result);
}
});
} else {
mReceiver.onPageResult(mResultType, result);
}
}
在這裏,我們可以看到mPostExecutor
的身影, 這就是前面通過setPostExecutor
方法設置的,不過這些都不重要。回調最後會走到PageResult.Receiver
的onPageResult
,那麼onPageResult
方法裏面做了啥事呢?
PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
// Creation thread for initial synchronous load, otherwise main thread
// Safe to access main thread only state - no other thread has reference during construction
@AnyThread
@Override
public void onPageResult(@PageResult.ResultType int resultType,
@NonNull PageResult<V> pageResult) {
// ······
List<V> page = pageResult.page;
if (resultType == PageResult.INIT) {
// 將數據存儲到mStorage
// ······
} else {
// 將數據存儲到mStorage
// ······
if (mShouldTrim) {
// 裁剪數據。
}
}
// ······
}
};
onPageResult
方法看上去挺複雜的,其實就只做了兩件事:
- 將數據存儲到
mStorage
中去,主要是區分了三種情況:INIT表示第一次加載數據;APPEND表示加載下一頁的數據;PREPEND表示加載上一頁的數據。- 裁剪數據。有人可能會有疑問,爲啥會有裁剪數據的操作,什麼才叫裁剪數據呢?這個先要介紹一下
PagedStorage
這個類。顧名思義,PagedStorage
的作用就是存儲數據的,用什麼樣的數據結構存儲數據呢?分頁加載當然就是一頁一頁的存儲,所以數據結構就是類似於ArrayList<ArrayList<Data>>
。PagedStorage
內部便是這樣的實現,裁剪數據的目的將一些沒必要的數據裁剪掉,比如說,某些用於佔位符的數據,在PagedStorage
內部就是一個PLACEHOLDER_LIST
對象,還就是裁剪一些某些爲null數據,在Config裏面有一個mMaxSize
的配置項,我們可以通過設置具體的數目,但是設置了這麼了一定的數目,那麼沒有還沒有加載的數據怎麼表示呢?PagedStorage
會通過null表示。這個我們可以從Adpater的getItemCount方法得到一定的答案。假設我們設置爲1000,那麼getItemCount
方法肯定返回1000,沒有加載的數據都是用null來表示的。mMaxSize
這個配置項實際上比較複雜,我們後面重點介紹。
至此,我們對加載第一頁數據的邏輯已經理解的差不多了。簡單的來說,就是在創建PagedList的時候會進行請求。我們需要注意的是,在loadInitial
方法裏面,區分異步加載和同步加載的不同點。
(2). 加載下一頁數據
由於ContiguousDataSource
存在dispatchLoadAfter
和dispatchLoadBefore
兩個不同的加載邏輯,這裏我將這兩個方法加載的數據統稱爲加載下一頁數據。
PagedListAdapter
在通過getItem
方法回去對應位置的數據時,會有一個特殊的調用,我們來看看具體的代碼--AsyncPagedListDiffer
的getItem
方法:
public T getItem(int index) {
// ······
mPagedList.loadAround(index);
// ······
}
下一頁數據的加載就是通過這裏來觸發的,我們來看看具體的實現:
public void loadAround(int index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
}
mLastLoad = index + getPositionOffset();
loadAroundInternal(index);
mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
/*
* mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to
* dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
* and accesses happen near the boundaries.
*
* Note: we post here, since RecyclerView may want to add items in response, and this
* call occurs in PagedListAdapter bind.
*/
tryDispatchBoundaryCallbacks(true);
}
loadAround
方法本身沒有做多少事,真正的操作都在loadAroundInternal
方法裏面,我們來看看具體的實現,這裏以ContiguousPagedList
爲例:
@MainThread
@Override
protected void loadAroundInternal(int index) {
int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount());
int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount() + mStorage.getStorageCount());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
if (mPrependItemsRequested > 0) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
}
loadAroundInternal
方法做的事實際上非常的簡單,就是判斷調用schedulePrepend
方法還是scheduleAppend
方法,主要是通過設置的預取距離跟當前所處的位置對比。這兩個就是用來分別觸發DataSource的dispatchLoadAfter
方法和dispatchLoadBefore
方法。
那麼,這兩個方法最後調用到哪裏呢?其實就是我們在自定義DataSource重寫的兩個方法:loadAfter
和loadBefore
。在這裏,我們需要的是注意的這兩個方法是在子線程裏面調用的,同時,在創建LoadCallbackImpl
時還設置了mainThreadExecutor
:
@Override
final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),
new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
}
跟loadInitial
方法不一樣的是,在loadBefore
方法調用的時候,mainThreadExecutor
已經不爲null了,所以在loadBefore
方法中,不要使用異步方法進行求網絡請求,主要出於如下兩個方面考慮:
- 儘量減少異步線程的數量。
loadBefore
方法本身就在子線程裏面調用的,我們沒有必要再去啓動線程,我們都知道系統的資源都是有限的,啓動一個線程還是比較消耗系統資源的。- 避免出現一些奇怪的問題。子線程裏面再去啓一個子線程,最後的回調接口最初的主線程裏面,中間垮了兩個線程,這個過程極易容易出現線程安全問題。
4. PagedList的Config配置
相信大家纔開始使用的PagedList的時候,在Config配置上踩了很多的坑。今天,我就在這裏重點介紹每個配置的作用。
字段名稱 | 解釋 |
---|---|
mPageSize | 每頁的大小,主要透傳到請求方法裏面,用來決定請求數據的數量。 |
mPrefetchDistance | 預取範圍,用來設置滑動什麼位置才請求下一頁的數據。 |
mInitialLoadSizeHint | 初始化請求數據的數量。 |
mEnablePlaceholders | 是否開啓佔位符,true表示開啓,false則表示不開啓。 |
mMaxSize | 數據的總數。 |
上面簡單的介紹了一下每個字段含義,接下來我們將詳細的解釋每個字段的作用。
(1). mPageSize
其實我們從這個名字裏面就可以知道,這個字段的含義就表示每頁的大小,其實我們在進行網絡請求的請求時,也完全沒必要通過mPageSize
字段決定請求數據的數量。
針對於兩個實現不同的DataSource,對mPageSize字段應用的程度也是不同的。其中ContiguousPagedList
沒有對mPageSize
做過多的要求,包括我們在請求數據的時候,也可以忽略這個字段(雖然可以這麼做,但是最好別這樣做)。
而TiledPagedList
對mPageSize
則是強依賴,從兩個方面來說:
首先,從初始化請求方面說起,在計算初始化的數量時,會通過mPageSize
來計算:
@WorkerThread
TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
int position) {
// ······
if (mDataSource.isInvalid()) {
// ······
} else {
final int firstLoadSize =
(Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize;
final int idealStart = position - firstLoadSize / 2;
final int roundedPageStart = Math.max(0, idealStart / pageSize * pageSize);
mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
pageSize, mMainThreadExecutor, mReceiver);
}
}
通過上面的代碼,我們可以發現,TiledPagedList
會將初始化頁面大小設置爲mPageSize
的整倍數。
需要特別注意的是:我們在loadInitial
方法設置請求數量,必須是mPageSize
的整數倍。因爲我們可以從onResult
方法裏面看到一個判斷:
@Override
public void onResult(@NonNull List<T> data, int position, int totalCount) {
if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
if (position + data.size() != totalCount
&& data.size() % mPageSize != 0) {
throw new IllegalArgumentException("PositionalDataSource requires initial load"
+ " size to be a multiple of page size to support internal tiling."
+ " loadSize " + data.size() + ", position " + position
+ ", totalCount " + totalCount + ", pageSize " + mPageSize);
}
if (mCountingEnabled) {
int trailingUnloadedCount = totalCount - position - data.size();
mCallbackHelper.dispatchResultToReceiver(
new PageResult<>(data, position, trailingUnloadedCount, 0));
} else {
// Only occurs when wrapped as contiguous
mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
}
}
}
那麼Google爸爸爲啥要千方百計的保證請求數據的數量是pageSize的整數倍呢?這個其實跟佔位符有關,一旦開啓了佔位符,Google爸爸就認爲每一頁請求都是應該是一樣的,所以當遇到不同的size時,比如說在初始化時時整數倍的pageSize,Google爸爸就會進行分頁。
比如說,mPageSize
爲20,第一次請求的數據有40條;那麼就會把這40條數據拆分成爲兩頁的數據,那麼在哪裏進行拆分的呢?就在PagedStorage
的initAndSplit
方法裏面:
void initAndSplit(int leadingNulls, @NonNull List<T> multiPageList,
int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) {
int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize;
for (int i = 0; i < pageCount; i++) {
int beginInclusive = i * pageSize;
int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize);
List<T> sublist = multiPageList.subList(beginInclusive, endExclusive);
if (i == 0) {
// Trailing nulls for first page includes other pages in multiPageList
int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size();
init(leadingNulls, sublist, initialTrailingNulls, positionOffset);
} else {
int insertPosition = leadingNulls + beginInclusive;
insertPage(insertPosition, sublist, null);
}
}
callback.onInitialized(size());
}
initAndSplit
方法很簡單,就是數據拆分爲一頁的一頁的存儲起來,保證每頁大小都是我們設置的mPageSize。
其次,再來看看加載下一頁的數據的請求,當請求到下一頁的數據,會通過PagedStorage
的insertPage
方法存儲起來,在insertPage方法裏面有一個特別的判斷:
public void insertPage(int position, @NonNull List<T> page, @Nullable Callback callback) {
final int newPageSize = page.size();
if (newPageSize != mPageSize) {
// differing page size is OK in 2 cases, when the page is being added:
// 1) to the end (in which case, ignore new smaller size)
// 2) only the last page has been added so far (in which case, adopt new bigger size)
int size = size();
boolean addingLastPage = position == (size - size % mPageSize)
&& newPageSize < mPageSize;
boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1
&& newPageSize > mPageSize;
// OK only if existing single page, and it's the last one
if (!onlyEndPagePresent && !addingLastPage) {
throw new IllegalArgumentException("page introduces incorrect tiling");
}
if (onlyEndPagePresent) {
mPageSize = newPageSize;
}
}
// ······
}
如果我們請求的數據大小不符合要求,直接回拋出異常。那麼什麼是不符合要求呢?就是請求返回的不爲mPageSize。
那麼爲什麼必須要保證每頁是一樣的,這裏我就簡單的介紹一下,感興趣的可以看看PagedStorage
的實現:
當我們的Adpter通過getItem方法獲取數據時,其實調用的是
PagedStorage
的get方法獲取。我們知道分頁數據其實是通過數組包裹數組的數據結構進行存儲數據的,所以在獲取數據時,需要獲取兩個Index,在PagedStorage
內部稱爲localIndex
和pageInternalIndex
,這兩個index一個是一維數組的index,一個是二維數組的index。如果每頁大小都是一樣的(這種情況在PagedStorage
內部被稱爲Tiled
),那麼就可以通過如下方式如下計算:
// it's inside mPages, and we're tiled. Jump to correct tile.
localPageIndex = localIndex / mPageSize;
pageInternalIndex = localIndex % mPageSize;
所以,這就是爲啥要保證每頁大小必須一樣的原因。
上面介紹那麼多,總結起來就是:初始化請求時,請求數據的總數必須是mPageSize的整數倍,加載下一頁時必須爲mPageSize。簡而言之,我們在請求傳參的時候,不要亂搞,避免出現各種問題,建議都傳mPageSize,即param裏面帶的那個Size。
(2). mPrefetchDistance
mPrefetchDistance
表示也是非常的簡單,就是表示預取距離,比如說,我們設置爲5,就表示滑動到倒數第5個的時候,我們才請求下一頁的數據。
我們先來看看ContiguousPagedList
的應用:
@MainThread
@Override
protected void loadAroundInternal(int index) {
int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount());
int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
mStorage.getLeadingNullCount() + mStorage.getStorageCount());
mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
if (mPrependItemsRequested > 0) {
schedulePrepend();
}
mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
if (mAppendItemsRequested > 0) {
scheduleAppend();
}
}
ContiguousPagedList
就在loadAroundInternal
方法裏面進行判斷的,具體的細節這裏我們就不深入的討論了。
我們再來看看PositionalDataSource
的實現:
public void allocatePlaceholders(int index, int prefetchDistance,
int pageSize, Callback callback) {
if (pageSize != mPageSize) {
if (pageSize < mPageSize) {
throw new IllegalArgumentException("Page size cannot be reduced");
}
if (mPages.size() != 1 || mTrailingNullCount != 0) {
// not in single, last page allocated case - can't change page size
throw new IllegalArgumentException(
"Page size can change only if last page is only one present");
}
mPageSize = pageSize;
}
final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
allocatePageRange(minimumPage, maximumPage);
int leadingNullPages = mLeadingNullCount / mPageSize;
for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
int localPageIndex = pageIndex - leadingNullPages;
if (mPages.get(localPageIndex) == null) {
//noinspection unchecked
mPages.set(localPageIndex, PLACEHOLDER_LIST);
callback.onPagePlaceholderInserted(pageIndex);
}
}
}
ContiguousPagedList
就在PagedStorage
的allocatePlaceholders
方法裏面進行判斷的,具體的細節這裏我們就不深入的討論了。說一句題外話,PagedStorage
很重要,如果想要理解PagedList的機制,一定要了解它,有機會我會專門的寫一篇文章來分析它。
(4).mInitialLoadSizeHint
這個字段的含義也非常的簡單,就是初始化請求數據的大小。這裏需要特別的是:ContiguousPagedList
的大小是設置的多少,請求數據時拿到就是多少;TiledPagedList
的大小要根據mInitialLoadSizeHint
設置的大小而定,如果mInitialLoadSizeHint
比mPageSize
大,那麼就是 2 * mPageSize。
(5). mEnablePlaceholders
這個字段表示的含義就是是否開啓佔位符,意思看上去非常的簡單,但是Paging的內部使用這個字段貫穿全文。相信大家在使用Paging的時候都有一個問題,就是當我們init方法裏面回調結果時,應該調用帶totalCount的onResult
方法,還是不帶toltalCount的onResult
方法?
同時,大家在使用PositionalDataSource
時,發現將mEnablePlaceholders
設置爲true,此時只能調用帶totalCountonResult
方法。在以前的版本中,這裏調用錯了,頁面什麼反應都沒有,現在還好,會拋異常了。這又是爲啥呢?
當我們不開啓佔位符時,爲啥不能用TiledPagedList
?我們在PagedList
的create方法中發現,當沒有開啓佔位符,儘管我們使用的是PositionalDataSource
,最後是還是會使用wrapAsContiguousWithoutPlaceholders
方法將PositionalDataSource
轉換成爲連續的DataSource,創建的PagedList也是ContiguousPagedList
。
接下來的內容,我們將一一的解答上面三個問題。
回到這個字段的本身,開啓佔位符到底表示什麼意思,可以看一下下面的效果圖:
佔位符的意思非常簡單,就是指有些Item的內容還沒有加載回來,先用一些默認的UI來表示,比如說,上圖中顯示
加載中
就是表示沒有數據還沒有加載回來。所以,在這裏,我們可以解釋上面的第三個問題。當我們沒有開啓佔位符的時候,Adapter通過getItem方法獲取的數據肯定不爲空,所以可以認爲每一頁的每一項數據都是有效,且是完整的,這個就比較符合連續性的數據的邏輯,同時連續的數據方便維護,因爲連續的數據通常不用進行trim,更不會使用null和類似於
PLACEHOLDER_LIST
這種來表示佔位,所以將其轉換成爲連續的數據類型是簡化實現。接下來,我們來看一下兩個
onResult
方法。熟悉Paging 的同學應該都知道,如果我們開啓了佔位符,一定要調用帶totalCount的方法?事實真是如此的嗎?這裏分別從ContiguousDataSource
和PositionalDataSource
來看下。在
ContiguousDataSource
及其子類中,我們會發現onResult
方法一共如下兩個:
public abstract void onResult(@NonNull List<Value> data);
public abstract void onResult(@NonNull List<Value> data, int position, int totalCount);
其實,在ContiguousDataSource
內部,不管是否開啓佔位符,帶totalCount
的onResult
方法都可以調用,只是有一定區別:
totalCount表示的意思,我們可以簡單的理解爲當前數據的總數。
- 當開啓開啓了佔位符。調用帶
totalCount
的onResult
方法,就表示當前數據總數一定爲totalCount
,Adapter的itemCount也會是totalCount
,此時getItem獲取的數據可能爲空;如果調用的是不帶totalCount
的onResult
方法,那麼Adapter的itemCount就是具體數據的數量,此時getItem獲取的肯定不爲空。- 當沒有開啓佔位符。兩個
onResult
方法沒有區別。
我們再來看看 PositionalDataSource
,在其內部兩個onResult
方法的定義如下:
public abstract void onResult(@NonNull List<T> data, int position);
public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
我們分別來看看這個兩個方法的區別:
- 當開啓了佔位符,只有調用帶
totalCount
的方法,調用另一個方法直接拋異常。需要特別注意的是,此時totalCount傳遞的最大值爲Int.MAX_VALUE - params.pageSize
,如下的代碼:
@WorkerThread
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<Message>
) {
val execute = RequestUtils.getService().getMessage(params.pageSize, 0).execute()
val messageList = execute.body()
val errorBody = execute.errorBody()
if (execute.code() == 200 && messageList != null && errorBody == null) {
callback.onResult(messageList, 0, Int.MAX_VALUE - params.pageSize)
} else {
callback.onResult(Collections.emptyList(), 0)
}
}
因爲如果我們傳遞Integer.MAX_VALUE
,在加載下一頁數據的時候,PagedStorage
計算數據時會溢出,這也是爲什麼當我們傳遞Integer.MAX_VALUE
,下一頁的數據沒有成功加載,溢出代碼如下:
public void allocatePlaceholders(int index, int prefetchDistance,
int pageSize, Callback callback) {
// ······
// 這裏會溢出
final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
allocatePageRange(minimumPage, maximumPage);
int leadingNullPages = mLeadingNullCount / mPageSize;
for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
int localPageIndex = pageIndex - leadingNullPages;
if (mPages.get(localPageIndex) == null) {
//noinspection unchecked
mPages.set(localPageIndex, PLACEHOLDER_LIST);
callback.onPagePlaceholderInserted(pageIndex);
}
}
}
- 當沒有開啓佔位符,兩個方法沒有區別。
(6). mMaxSize
這個字段表示的意思雖然是數據的總數,但是實際上,這個字段極其的坑人。我們在使用這個字段時,發現不僅不會生效,而且設置了之後還是出現各種問題:
- 假設我們在使用
ContiguousDataSource
設置爲100,沒有開啓佔位符的話,會出現混亂的問題,具體效果如下:
開啓佔位符的話,mMaxSize不生效,具體的效果如下:
- 假設我們在使用
ContiguousDataSource
設置爲100,沒有開啓佔位符的效果跟ContiguousDataSource
,數據會混亂;開啓佔位符的話,mMaxSize就生效了,這也是唯一生效的地方,具體效果:
簡單的來說,這個配置極其的不好用,同時Google爸爸在方法上也進行了特別的註釋,Google爸爸說:mMaxSize只能盡力而爲,不能百分百的保證。可想而知,這個配置是多麼的雞肋。
需要特別的注意的是:在mMaxSize
唯一生效的地方,如果我們設置的mMaxSize
和totalCount是不一樣的值,那麼就以totalCount
爲準。
所以,如果我們要限制大小的話,最好是自己來實現,不要使用這個字段。
5. 總結
到這裏,本文的內容就到此結束了,其實關於pagin的內容不僅僅是這些,本文的內容只能說起到一個提綱挈領的作用,比如說,PagedStorage
的設計,這部分內容並沒有深入的介紹,有興趣的同學可以去看看,我相信大家理解這個類所做的事,對Paging
的理解會更加深入。最後我來簡單的總結一下本文的內容:
- 在Paging中,我們可以
PagedList
和DataSource
分爲兩類:非連續的和連續的,兩者其實沒有本質上的區別,只是在一些特殊業務場景上可能會有一點區別,比如說佔位符,非連續的數據如果沒有開啓佔位符的特性,其實本質上跟連續的數據是一樣的。- 初始化請求數據,是在PagedList的構造方法裏面進行,其中初始化請求方法本身在子線程裏面執行,所以我們直接使用同步方法進行網絡請求即可(當然也可以使用異步方法,但是不推薦。);下一頁數據的請求時機,是在getItem方法裏面的觸發,PagedList會根據position來決定是否請求下一頁的數據。
- Config的
mPageSize
用來限制每頁數據的大小,同時我們在網絡請求時,一定要使用給定的size,不要想着搞各種騷操作,避免出現各種問題。- Config中的
mEnablePlaceholders
用來控制是否使用佔位符。ContiguousDataSource
和PositionalDataSource
對於開啓佔位符有不同的要求。ContiguousDataSource
在網絡請求回調的時候,兩個onResult
方法都可以使用,本質上並沒有什麼區別,只是要注意的是當調用帶toltalCount
的onResult
方法是,getItem可能返回爲null,這個在使用的時候需要特別關心;PositionalDataSource
開啓了佔位符,只能調用帶toltalCount
的onResult
方法。- 如果使用的是
PositionalDataSource
,onResult
方法中的toltalCount
的值不要超過Integer.MAX_VALUE - pageSize
,因爲在計算位置的時候可能會溢出,導致不能加載下一頁的數據。- Config中的
mMaxSize
用來限制總數據的大小,但是實際上作用範圍非常的小,只在PositionalDataSource
開啓佔位符才生效,同時如果toltalCount
跟mMaxSize
不一樣的,會以toltalCount
爲準。總之來說,不要輕易的使用mMaxSize
。
最後,我想簡單的說幾句話,paging是爲了解決分頁加載的問題而出現,這個初衷是很好的,但是使用的門檻實在是太高了,稍稍不注意就可能出現出錯誤,比如說Config的配置,onResult的回調。同時,我覺得paging在代碼設計上也有一定的問題,比如說區分連續和非連續的,這個直接導致實現DataSource和PagedList的工作量翻倍;PagedStorage
將各種代碼和實現糅合在一個類裏面,導致閱讀起來特別費勁。不過最近有一個好消息的是,Google爸爸在最新的JetPack推出了paging3,我希望這些問題都已經解決了。