Android分页组件Paging简单使用


title: Android分页组件Paging简单使用
date: 2018-10-10 17:03:23
tags: Paging


2018-12.21更新

发现loadRange()方法似乎是在线程中执行的,所以这个方法内通过同步网络请求获取数据,重新修改了代码,提供了一些之前忽略的代码。

1.简介:

Paging组件是Google新推出的分页组件,可以轻松帮助开发者实现RecyclerView中分页加载功能。
本文先开坑,等以后用到在详细写明。
推荐博客:https://www.jianshu.com/p/1bfec9b9612c

2.Paging库引入:

在gradle中引入:
implementation 'android.arch.paging:runtime:1.0.0'

刚引入就遇到了错误,真是一个不友好的开始:

Error:Execution failed for task ':app:transformDexArchiveWithExternalLibsDexMergerForDebug'.
> java.lang.RuntimeException: java.lang.RuntimeException: com.android.builder.dexing.DexArchiveMergerException: Unable to merge dex

目测,是导入重复的包,这个出错的地方还是google自己的东西。
“大水冲了龙王庙,自家人打自家人”?
出现冲突的是下面这个包:

compile 'com.android.support:design:26.1.0'

google了一下,在stackoverflow中找到了解决办法:
https://stackoverflow.com/questions/49028119/multiple-dex-files-define-landroid-support-design-widget-coordinatorlayoutlayou
即用编译版本改成27,并且把对应的库版本也改为27
implementation 'com.android.support:appcompat-v7:27.1.1'
compile 'com.android.support:design:27.1.1'
问题了解决了,接下来我们看看他的工作原理

3.工作原理

Android分页组件Paging简单使用_第1张图片
PagingWork.gif

这是官方提供的原理图,很清楚地看到从DataSource到PagedList, PagedListAdapter最后是我们的recyclerview,我们先眼熟这几个名字,下文会常常出现。
这里采用的是MVVM架构。

3.1 Adapter构建

public class AdapterPaging extends PagedListAdapter {

    private Context mContext;

    public static final DiffUtil.ItemCallback mDiffCallback = new AdapterPaging.VideoInfoItemCallback();

    protected AdapterPaging() {
        super(mDiffCallback);
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        VideoInfo videoInfo = getItem(position);
    }
    
    static class ViewHolder extends RecyclerView.ViewHolder{

        private LinearLayout ll_videoInfo;

        public ViewHolder(View itemView) {
            super(itemView);
            ll_videoInfo = itemView.findViewById(R.id.ll_video_info);
        }
    }

    
    private static class VideoInfoItemCallback extends DiffUtil.ItemCallback{
        @Override
        public boolean areItemsTheSame(VideoInfo oldItem, VideoInfo newItem) {
            return oldItem.getUrl() == newItem.getUrl();
        }

        @Override
        public boolean areContentsTheSame(VideoInfo oldItem, VideoInfo newItem) {
            return (oldItem == newItem);
        }
    }
}

大致的4个不同如下:

  1. Adapter不再继承自RecyclerView.Adapter,改为继承自PagedListAdapter,因为PagedListAdapter就是RecyclerView.Adapter的一个子类。
  2. 定义内部回调接口VideoInfoItemCallback继承自DiffUtil.ItemCallback,并且实例化一个父类引用指向子类(VideoInfoItemCallback)对象
    public static final DiffUtil.ItemCallback mDiffCallback = new AdapterPaging.VideoInfoItemCallback();
  3. 重写构造方法,无需参数传入,调用父类构造方法将mDiffCallback传入。
  4. 通onBindViewHolder中过调用getItem(position);获得指定位置的数据对象。
    因为Adapter中不再需要维护一个数据List了,PagedListAdapter中已经维护有,并且提供getItem()方法访问。

3.2 在Activity中的使用

在使用Paging后,我们无需向Adapter中在传入数据源List,我们需要构造LiveData。
LiveData需要DataSource.Factory对象和PagedList.Config对象,只是实例化DataSource.Factory对象需要额外两个步骤
DataSource.Factory是一个抽象类,实例化时需要实现create()函数,这个函数返回值是一个DataSource类对象
DataSource是一个抽象类,他有三个实现子类:(详细参考原博客:https://www.jianshu.com/p/95d44c5338fd)

(1)PageKeyedDataSource 按页加载,如请求数据时传入page页码。
(2)ItemKeyedDataSource 按条目加载,即请求数据需要传入其它item的信息,如加载第n+1项的数据需传入第n项的id。
(3)PositionalDataSource 按位置加载,如加载指定从第n条到n+20条。

所以我们捋一下思路,
1)首先要定义一个MyDataSource继承自DataSource的三个子类之一,
2)再定义一个MyDataSourceFactory继承自DataSource.Factory,返回值是MyDataSource
3)然后实例化PagedList.Config,这个类提供有Builder(),比较简单。
4)最后将MyDataSourceFactory对象和PagedList.Config对象传入new LivePagedListBuilder()中得到liveData数据源。
将liveData数据源和Adapter绑定是通过观察者模式实现,调用liveData.observe()。

//定义MyDataSource类,继承自DataSource三个子类之一
    private class MyDataSource extends PositionalDataSource{

        @Override
        public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) {
            callback.onResult(initData(), 0, 10);//initData()是我封装的加载数据方法
        }

        @Override
        public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback callback) {
            callback.onResult(syncRequestData().getSubjects());
        }
    }
    //定义MyDataSourceFactory,是DataSource.Factory的实现类
    private class MyDataSourceFactory extends DataSource.Factory{

        @Override
        public DataSource create() {
            return new MyDataSource();
        }
    }
    //将生产config和liveData的代码封装在这个方法中
    private void initPaging(){
        PagedList.Config config = new PagedList.Config.Builder()
                .setPageSize(10)    //每页显示的词条数
                .setEnablePlaceholders(false)
                .setInitialLoadSizeHint(10) //首次加载的数据量
                .setPrefetchDistance(5)     //距离底部还有多少条数据时开始预加载
                .build();

        /**
         * LiveData用LivePagedListBuilder生成
         * LivePagedListBuilder 构造方法需要 DataSource.Factory和PagedList.Config
         */
        LiveData> liveData = new LivePagedListBuilder(new MyDataSourceFactory(), config)
                .build();
        //观察者模式,将Adapter注册进去,当liveData发生改变事通知Adapter
        liveData.observe(this, new Observer>() {
            @Override
            public void onChanged(@Nullable PagedList subjectsBeans) {
                adapterHomeInfo.submitList(subjectsBeans);
            }
        });
    }

从上面的代码我们可以看到,数据的入口在MyDataSource的两个重写方法里:
initData():就是用于数据加载的方法,可以按自己想需求来封装。
syncRequestData():用于"加载更多"的方法,loadRange()似乎本身就处在子线程中,所以此处可以用同步网络请求。
initData()在初始化时调用,所以要求我们初始化的时候使用的是本地数据,而来不及进行网络请求。

4.加载网络数据

这么好的控件,我无论如何都想用于纯网络加载。想这么做也很简单,因为我们已经把与初始化相关的代码封装到initPaging()中了。
我们只需要在合适的时候进行初始化,就能达到延迟加载的目的了。比如,我们可以用handler和网络请求实现消息异步处理,在handler的中调用initPaging(),这样就可以不需要本地数据进行初始化了。


public static final String MOVIE_REQUEST= 1;
private Movie movie;
private MyHandler handler = new MyHandler(this);
//定义一个MyHandler,用弱引用防止内存泄漏
private static class MyHandler extends Handler {
    WeakReference weakReference;

    public MyHandler(ActivityHome activityHome){
        weakReference = new WeakReference(activityHome);
    }

    @Override
    public void handleMessage(Message msg) {
        ActivityHome activity = weakReference.get();
        //网络状况不好时,返回obj为null
        if(null == msg.obj){
            Toast.makeText(activity, "获取数据失败,请检查网络", Toast.LENGTH_SHORT).show();
            return;
        }
        switch (msg.what){
            case MOVIE_REQUEST:
                activity.movie = NetWorkUtil.parseJsonWithGson(msg.obj.toString(), Movie.class);
                activity.initPaging();
                break;

            default:
                break;
        }
    }
}
/**
 * 初始化数据
 * @return
 */
private List initData(){
    List subjectsBeanList = new ArrayList<>();
    subjectsBeanList = movie.getSubjects();
    return subjectsBeanList;
}
/**
 * 进行网络请求,同步
 * @return
 */
private Movie syncRequestData(){
        //将uri中pageindex对应的参数+1,然后进行同步网络请求
        int currentPageNumber = Integer.parseInt(uri.getQueryParameter("pageindex"));
        String url = replace(uri.toString(), "pageindex", currentPageNumber+1+"");
        return NetWorkUtil.syncRequest(url, Movie.class);
}
/**
 * 进行网络请求,异步
 * @return
 */
private void asynRequestData(MyHandler handler){
        //将uri中pageindex对应的参数+1,然后进行异步网络请求
        int currentPageNumber = Integer.parseInt(uri.getQueryParameter("pageindex"));
        String url = replace(uri.toString(), "pageindex", currentPageNumber+1+"");
        NetWorkUtil.sendRequestWithOkHttp(url, MOVIE_REQUEST, handler);
}

//将Uri中的参数重新赋值
public static String replace(String url, String key, String value) {
    if (!TextUtils.isEmpty(url) && !TextUtils.isEmpty(key)) {
        url = url.replaceAll("(" + key + "=[^&]*)", key + "=" + value);
    }
    return url;
}

代码完成了,思路很简单。
我们只需要在onCreate()中调用asynRequestData()方法,开始请求数据,数据得到后开始初始化Paging组件,之后通过同步网络请求获得后续的数据。
关于同步网络请求syncRequest()和异步网络请求sendRequestWithOkHttp()我就不在这里给出了,大家可以用不同的库去实现。

5.结束

又到招聘的季节了,给大家发个字节跳动的内推福利:
内推码:UDXTM7B
投递链接:https://job.toutiao.com/campus/
欢迎大家投递。

你可能感兴趣的:(Android分页组件Paging简单使用)