阅读 1046

Paging Library使用及原理

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