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.工作原理
这是官方提供的原理图,很清楚地看到从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个不同如下:
- Adapter不再继承自RecyclerView.Adapter,改为继承自PagedListAdapter,因为PagedListAdapter就是RecyclerView.Adapter的一个子类。
- 定义内部回调接口VideoInfoItemCallback继承自DiffUtil.ItemCallback
,并且实例化一个父类引用指向子类(VideoInfoItemCallback)对象
public static final DiffUtil.ItemCallback
mDiffCallback = new AdapterPaging.VideoInfoItemCallback(); - 重写构造方法,无需参数传入,调用父类构造方法将mDiffCallback传入。
- 通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/
欢迎大家投递。