Paging Library使用及原理
簡介
paging是google推出的分頁載入框架,收錄在 jetpack開發套件,結合RecycleView使用,開發者只用選擇合適的模板實現自己的DataSource(資料儲存層,可以是記憶體/db/網路),框架層實現了自動分頁載入的邏輯,詳情可以參考官方文件: developer.android.com/topic/libra…
Demo示例
先來一個簡單的示例,分頁載入學生列表,模擬了100個學生資料,id從0開始自增,以id為cursor分頁載入,每頁10條資料 效果如下:
dependencies {
...
implementation ("android.arch.paging:runtime:1.0.1")
implementation 'android.arch.lifecycle:extensions:1.1.1'
}
複製程式碼
-
示例程式碼
-
選擇合適的DataSource
- 一共3種DataSource可選,取決於你的資料是以何種方式分頁載入:
- ItemKeyedDataSource:基於cursor實現,資料容量可動態自增
- PageKeyedDataSource:基於頁碼實現,資料容量可動態自增
- PositionalDataSource:資料容量固定,基於index載入特定範圍的資料
- 學生資料以id自增排序,以id作為分頁載入的cursor,所以這裡我們選擇ItemKeyedDataSource
public class StudentDataSource extends ItemKeyedDataSource<String, StudentBean> { private static final int MIN_STUDENT_ID = 1; private static final int MAX_STUDENT_ID = 100; private Random mRandom = new Random(); public StudentDataSource() { } @Override public void loadInitial(@NonNull LoadInitialParams<String> params, @NonNull LoadInitialCallback<StudentBean> callback) { List<StudentBean> studentBeanList = mockStudentBean(0L, params.requestedLoadSize); callback.onResult(studentBeanList); } @Override public void loadAfter(@NonNull LoadParams<String> params, @NonNull LoadCallback<StudentBean> callback) { long studentId = Long.valueOf(params.key); int limit = (int)Math.min(params.requestedLoadSize, Math.max(MAX_STUDENT_ID - studentId, 0)); List<StudentBean> studentBeanList = mockStudentBean(studentId + 1, limit); callback.onResult(studentBeanList); } @Override public void loadBefore(@NonNull LoadParams<String> params, @NonNull LoadCallback<StudentBean> callback) { long studentId = Long.valueOf(params.key); int limit = (int)Math.min(params.requestedLoadSize, Math.max(studentId - MIN_STUDENT_ID, 0)); List<StudentBean> studentBeanList = mockStudentBean(studentId - limit, limit); callback.onResult(studentBeanList); } @NonNull @Override public String getKey(@NonNull StudentBean item) { return item.getId(); } } 複製程式碼
- 一共3種DataSource可選,取決於你的資料是以何種方式分頁載入:
-
實現DataSource工廠(可選,Demo使用了LivePagedListBuilder,依賴Factory)
- 這裡實現的工廠邏輯很簡單,只是例項化一個DataSource
public class StudentDataSourceFactory extends DataSource.Factory<String, StudentBean> { @Override public DataSource<String, StudentBean> create() { return new StudentDataSource(); } } 複製程式碼
-
生成PageList
- 生成PageList有連個必要引數
- DataSource:前面已經介紹過
- PagedList.Config,包含以下配置:
- pageSize:每頁載入數量
- prefetchDistance:提前多少個item開始載入下(上)一頁資料,預設為pageSize
- initialLoadSizeHint:初始化多少條資料,預設是pageSize*3
- enablePlaceholders:是否支援佔位符顯示(只有列表size固定的情況下有效)
- 依賴LivePagedListBuilder生成LiveData(持有一個PageList例項)
public class StudentRepositoryImpl implements IStudentRepository { @Override public LiveData<PagedList<StudentBean>> getAllStudents() { int pageSize = 10; StudentDataSourceFactory dataSourceFactory = new StudentDataSourceFactory(); PagedList.Config pageListConfig = new PagedList.Config.Builder() .setEnablePlaceholders(false) .setInitialLoadSizeHint(pageSize * 2) .setPageSize(pageSize) .build(); return new LivePagedListBuilder<>(dataSourceFactory, pageListConfig) .build(); } } 複製程式碼
- builde內部構建PageList程式碼如下:
mList = new PagedList.Builder<>(mDataSource, config) .setNotifyExecutor(notifyExecutor) .setFetchExecutor(fetchExecutor) .setBoundaryCallback(boundaryCallback) .setInitialKey(initializeKey) .build(); 複製程式碼
- 生成PageList有連個必要引數
-
實現PagedListAdapter
- 和ListAdaper一樣,需要需要自定義Diff規則
public class StudentAdapter extends PagedListAdapter<StudentBean, StudentViewHolder> {
private static final DiffUtil.ItemCallback<StudentBean> DIFF_CALLBACK = new ItemCallback<StudentBean>() {
@Override
public boolean areItemsTheSame(StudentBean oldItem, StudentBean newItem) {
return TextUtils.equals(oldItem.getId(), newItem.getId());
}
@Override
public boolean areContentsTheSame(StudentBean oldItem, StudentBean newItem) {
return oldItem == newItem;
}
};
public StudentAdapter() {
super(DIFF_CALLBACK);
}
@Override
public StudentViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.student_item, null, false);
return new StudentViewHolder(itemView);
}
@Override
public void onBindViewHolder(StudentViewHolder holder, int position) {
holder.bindData(getItem(position));
}
複製程式碼
- 繫結PageList到PagedListAdapter
StudentViewModel viewModel = ViewModelProviders.of(this).get(StudentViewModel.class);
viewModel.getPageListLiveData().observe(this, new Observer<PagedList<StudentBean>>() {
@Override
public void onChanged(@Nullable PagedList<StudentBean> studentBeans) {
studentAdapter.submitList(studentBeans);
}
});
複製程式碼
原始碼分析
-
大致流程如下:
- 條件觸發DataSource載入資料,包含兩種場景:
- PagedList被建立的時候,會呼叫DataSource載入初始資料(在當前執行緒執行)
- 使用者滾動列表(距離當前頁底部一定距離),自動觸發載入下一頁資料(預設使用arch框架定義的IO執行緒池)
- 資料載入完畢,回撥到PagedList儲存
- PagedList資料發生變化,通知到PagedListAdapter
- PagedListAdapter內部使用DiffUtil計算資料變化(發生在非同步執行緒,不會阻塞UI)
- DiffUtil計算完畢,notify到RecycleView進行區域性重新整理
- 條件觸發DataSource載入資料,包含兩種場景:
-
核心類圖
-
DataSource
- DataSource與PageList組合使用,不同的DataSource子類適用於不同的PageList子類
- ContiguousDataSource:可以動態擴容,基於"頁碼"或"遊標"進行分頁載入
- ItemKeyedDataSource:基於cursor分頁載入,抽象類,子類需要實現loadXXX載入資料
- PageKeyedDataSource:基於頁碼分頁載入,抽象類,子類需要實現LoadXXX載入資料
- PositionalDataSource:基於postion分頁載入特定範圍的資料
- LimitOffsetDataSource:基於DB實現的固定size的資料來源,依賴Room,抽象類,子類需要實現convertRows將cursor轉換成資料Bean
- TiledDataSource:Room1.0版本依賴這個型別,後續可能會替換成PositionalDataSource,抽象類,Room框架apt自動生成程式碼
- ContiguousDataSource:可以動態擴容,基於"頁碼"或"遊標"進行分頁載入
- DataSource支援map變換,類似RxJava的map,可以對value進行型別轉換,生成一個新的DataSource(其實是WrapperXXXDataSource包裝類,內部依賴Function<List, List>對資料進行轉換),map是抽象介面,需要由子類實現具體的變換規則
- Factory工廠介面,需要結合LivePagedListBuilder使用
-
PagedList
-
兩種型別的PagedList
- ContiguousPagedList:持有ContiguousDataSource例項,顧名思義,可以動態擴容,基於"頁碼"或"遊標"進行分頁載入
- TiledPagedList:持有PositionalDataSource例項,固定size,基於postion分頁載入特定範圍的資料
-
PagedList內部持有以下幾個重要成員變數
- Executor:執行緒排程器,用於執行資料載入和回撥介面
- Boundarycallback:觸發邊界的回撥
- PagedListConfig:配置引數
- PagedStorage:真正儲存資料的地方
- 內部以頁為單位儲存資料
- 資料變更後的通知回撥,用來通知UI更新
-
AsyncPagedListDiffer與PagedListAdapter
- AsyncPagedListDiffer:對新舊PagedList進行差分對比
- PagedListAdapter:持有AsyncPagedListDiffer例項,接收PagedList傳遞給AsyncPagedListDiffer進行差分對比並重新整理
-
資料載入流程圖
- 以Demo使用的ItemKeyedDataSource為例,載入下一頁的程式碼呼叫流程如下:
- 你RecycleView滾動過程中會觸發PagedListAdapter#getItem,間接呼叫AsyncPagedListDiffer#getItem
- AsyncPagedListDiffer內部持有一個PagedList例項,呼叫ContiguousPagedList#loadAround(該方法在父類PagedList實現),嘗試載入下一頁資料
- ContiguousPagedList繼而呼叫loadAroundInternal,判斷當前是否觸達邊界(邊界取決於prefetchDistance,例如prefetchDistance=5,當前已載入20條資料,那麼,當getItem的index>=15就會觸發下一頁資料載入),如果觸發,則非同步執行抽象方法dispatchLoadAfter載入下一頁資料
- ItemKeyedDataSource實現了dispatchLoadAfter,內部同步呼叫抽象方法loadAfter(具體的業務程式碼,Demo中對應StudentDataSource)真正載入資料
- 資料載入完畢,執行dispatchResultToReceiver將結果回傳
- 首先會呼叫ContiguousPagedList內部持有的Receiver例項的onPageResult
- 然後,呼叫PagedStorage#appendPage,將新的一頁資料追加在末尾
- PagedStorage處理完資料,回撥onPageAppended(ContiguousPagedList實現了該介面)
- ContiguousPagedList呼叫notifyChanged/notifyInserted通知所有的觀察者
- 觀察者AsyncPagedListDiffer收到onInserted/onChanged通知,再通知給PagedListAdapter重新整理RecycleView
- 開發者不用監聽RecyeleView的滾動來載入下一頁,所有的過程全部自動完成,開發者只需要關注自定義的DataSource,按照分頁規則,實現資料載入介面即可
後續
理想中的分頁載入庫只需要使用者關注業務資料結構,寫少量的程式碼及UI佈局,即可實現分頁載入的效果,後續打算基於Paging Libaray封裝一套基於"通用分頁協議"的"模板程式碼"
- 該分頁開發框架包含以下內容:
- 一套通用的分頁協議,與服務端協定
- 依賴網路庫及JSON解析庫實現預設的網路請求及資料解析
- 依賴ORM的DB方案,實現分頁資料的持久化
- 依賴Paging Library實現分頁資料快取/載入/通知更新等一系列動作
- 一定的擴充套件能力,譬如:資料的裝飾,去重,重排等
- 一定的配置能力,譬如:是否持久化以及持久化的頁數
- 開發者只需要遵循以下幾個步驟即可:
- 遵循通用的分頁協議,與服務端協定item資料結構
- 定義item資料Bean
- 自定義擴充套件能力(可選)
- 引數配置(可選)
- 通過資料Bean的class型別,生成PagedListAdapter
- 自定義檢視佈局,包括RecycleView以及ItemView,繫結Adapter