Paging Library使用及原理

Charles007發表於2019-05-11

Paging Library使用及原理

簡介

paging是google推出的分頁載入框架,收錄在 jetpack開發套件,結合RecycleView使用,開發者只用選擇合適的模板實現自己的DataSource(資料儲存層,可以是記憶體/db/網路),框架層實現了自動分頁載入的邏輯,詳情可以參考官方文件: developer.android.com/topic/libra…

Demo示例

先來一個簡單的示例,分頁載入學生列表,模擬了100個學生資料,id從0開始自增,以id為cursor分頁載入,每頁10條資料 效果如下:

gif5新檔案.gif | center | 282x500

  • 新增gradle依賴

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();
        }
      
    }
    複製程式碼
  • 實現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();
    複製程式碼
  • 實現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);
            }
        });
複製程式碼
  • 分頁載入日誌

image | left | 827x266

原始碼分析

  • 資料載入原理圖

7293029-27facf0a399c66b8.gif | center | 800x450

  • 大致流程如下:

    • 條件觸發DataSource載入資料,包含兩種場景:
      • PagedList被建立的時候,會呼叫DataSource載入初始資料(在當前執行緒執行)
      • 使用者滾動列表(距離當前頁底部一定距離),自動觸發載入下一頁資料(預設使用arch框架定義的IO執行緒池)
    • 資料載入完畢,回撥到PagedList儲存
    • PagedList資料發生變化,通知到PagedListAdapter
    • PagedListAdapter內部使用DiffUtil計算資料變化(發生在非同步執行緒,不會阻塞UI)
    • DiffUtil計算完畢,notify到RecycleView進行區域性重新整理
  • 核心類圖

  • DataSource

paging_datasource(1).png | center | 827x670

  • 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自動生成程式碼
  • DataSource支援map變換,類似RxJava的map,可以對value進行型別轉換,生成一個新的DataSource(其實是WrapperXXXDataSource包裝類,內部依賴Function<List, List>對資料進行轉換),map是抽象介面,需要由子類實現具體的變換規則
  • Factory工廠介面,需要結合LivePagedListBuilder使用
  • PagedList

paging_pagelist(2).png | center | 827x482

  • 兩種型別的PagedList

    • ContiguousPagedList:持有ContiguousDataSource例項,顧名思義,可以動態擴容,基於"頁碼"或"遊標"進行分頁載入
    • TiledPagedList:持有PositionalDataSource例項,固定size,基於postion分頁載入特定範圍的資料
  • PagedList內部持有以下幾個重要成員變數

    • Executor:執行緒排程器,用於執行資料載入和回撥介面
    • Boundarycallback:觸發邊界的回撥
    • PagedListConfig:配置引數
    • PagedStorage:真正儲存資料的地方
      • 內部以頁為單位儲存資料
      • 資料變更後的通知回撥,用來通知UI更新
  • AsyncPagedListDiffer與PagedListAdapter

    • AsyncPagedListDiffer:對新舊PagedList進行差分對比
    • PagedListAdapter:持有AsyncPagedListDiffer例項,接收PagedList傳遞給AsyncPagedListDiffer進行差分對比並重新整理
  • 資料載入流程圖

paging_flow.png | center | 827x649

  • 以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

相關文章