Paging Library使用及原理

Paging Library使用及原理

簡介

paging是google推出的分頁加載框架,收錄在 jetpack開發套件,結合RecycleView使用,開發者只用選擇合適的模板實現本身的DataSource(數據存儲層,能夠是內存/db/網絡),框架層實現了自動分頁加載的邏輯,詳情能夠參考官方文檔: developer.android.com/topic/libra…android

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();
        }
    }
    
    複製代碼
  • 生成PageListapp

    • 生成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

  • 大體流程以下:dom

    • 條件觸發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內部持有如下幾個重要成員變量ide

    • 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
相關文章
相關標籤/搜索