在从网络中加载数据时,通常的做法是将全部的数据加载到页面上显示,这种情况的坏处就在于,消耗流量和内存,尤其是在加载数据量比较大的时候,可以考虑分页加载数据,刚开始的时候可能只需要显示20条数据(总数100条),等到当前的数据处理完成之后,如果还需要后面的数据,那么再去加载,也就是“按需加载”的思想。
在JectPack组件中,提供了Paging来分页加载数据。目前获取数据的方式大都是通过网络获取数据或者从数据库获取数据,像从网络获取数据时,返回的数据大部分都是分页的:
{"data":{"curPage":1,"datas":[{"apkLink":"","audit":1,"author":"","canEdit":false,"chapterId":502,"chapterName":"自助","collect":false,"courseId":13,"desc":"","descMd":"","envelopePic":"","fresh":true,"id":12154,"link":"https://juejin.im/post/5e5b50eb6fb9a07cae136773","niceDate":"4小时前","niceShareDate":"4小时前","origin":"","prefix":"","projectLink":"","publishTime":1583112725000,"selfVisible":0,"shareDate":1583112725000,"shareUser":"JsonChao","superChapterId":494,"superChapterName":"广场Tab","tags":[],"title":"【建议收藏】2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂","type":0,"userId":611,"visible":1,"zan":0},{"apkLink":"","audit":1,"author":"","canEdit":false,"chapterId":502,"chapterName":"自助","collect":false,"courseId":13,"desc":"","descMd":"","envelopePic":"","fresh":false,"id":12137,"link":"https://blog.csdn.net/willway_wang/article/details/104525652","niceDate":"2天前","niceShareDate":"2天前","origin":"","prefix":"","projectLink":"","publishTime":1582947117000,"selfVisible":0,"shareDate":1582946392000,"shareUser":"willwaywang6","superChapterId":494,"superChapterName":"广场Tab","tags":[],"title":"代理模式学习笔记","type":0,"userId":833,"visible":1,"zan":0},{"apkLink":"","audit":1,"author":"","can
{"data":{"curPage":2,"datas":[{"apkLink":"","audit":1,"author":"","canEdit":false,"chapterId":502,"chapterName":"自助","collect":false,"courseId":13,"desc":"","descMd":"","envelopePic":"","fresh":false,"id":12049,"link":"https://blog.csdn.net/huangliniqng/article/details/104208227","niceDate":"2020-02-25 21:56","niceShareDate":"2020-02-23 18:58","origin":"","prefix":"","projectLink":"","publishTime":1582639016000,"selfVisible":0,"shareDate":1582455492000,"shareUser":"Huanglinqing","superChapterId":494,"superChapterName":"广场Tab","tags":[],"title":"自定义View二篇,如何自定义一个规范的ViewGroup","type":0,"userId":31874,"visible":1,"zan":0},{"apkLink":"","audit":1,"author":"","canEdit":false,"chapterId":502,"chapterName":"自助","collect":false,"courseId":13,"desc":"","descMd":"","envelopePic":"","fresh":false,"id":12069,"link":"https://juejin.im/post/5e4ff123e51d4527255ca2e1","niceDate":"2020-02-25 14:57","niceShareDate":"2020-02-25
“curPage”这个字段就代表当前页面的页码数,在执行网络请求时,往往通过页码数,来请求对应页面的数据,之前想要加载其他页面的数据,往往通过下拉刷新请求,但是在JectPack推出Paging之后,就能够实现数据的分页加载,而且对于一页的数据,也可以分批加载。
1、Paging
中的概念
首先使用Paging之前,需要添加依赖。
def paging_version = "2.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"
在Paging中,有几个比较重要的概念:DataSource、DataSource.Factory、PageList、PageListAdapter。
(1)DataSource
:数据源;为列表控件提供数据,往往需要将该数据转换为LiveData
类型,提供给RecyclerView
显示,数据源的种类分为3种:
----PageKeyedDataSource
:这是使用最多的数据源,往往用户系统API已经为我们将数据分好页,就像上面的Json数据一样;这是一个抽象的泛型类,需要传入两个泛型参数。
* @param <Key> Type of data used to query Value types out of the DataSource.
* @param <Value> Type of items being loaded by the DataSource.
*/
public abstract class PageKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
Key
:指的是分类的标准,比如说是页码,就是通过Key存储,供分页加载使用;
value
:数据的类型,往往是显示的数据。
后面具体的实例会详细介绍。
-----ItemKeyedDataSource
:功能和PageKeyedDataSource
类似,同样也是传入键值对,通过key去请求后台的数据。
-----PositionalDataSource
:这个和上述的分页不同的是,数据是混合在一起的,并没有分页,而是一个大数据堆,系统加载不能一次全部加载,可以使用PositionalDataSource
,分几次加载数据。
(2)DataSource.Factory
顾名思义,用于创建DataSource
的工厂类,主要用户生成LiveData<PageList<>>
时使用。
(3)PageList
、PageListAdapter
用于RecyclerView
显示的数据集合,RecyclerView
将不再使用原先的RecyclerView.Adapter
,而是使用PageListAdapter
,当订阅LiveData时,调用适配器的submitList
,提供数据给RecyclerView
显示。
2、Paging分页加载框架
(1)自定义数据源DataSource
public class MyPagingSource2 extends PageKeyedDataSource<Integer, ArticleBean.DataBean.DatasBean> {
//加载的页码
private int page;
//page不需要在此赋值,因为在api接口中需要传入Page,所以在请求数据时初始化即可
public MyPagingSource2(int page){
this.page = page;
}
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, ArticleBean.DataBean.DatasBean> callback) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Constant.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
IMainService iMainService = retrofit.create(IMainService.class);
Call<ArticleBean> call = iMainService.getArticle(page);
call.enqueue(new Callback<ArticleBean>() {
@Override
public void onResponse(Call<ArticleBean> call, Response<ArticleBean> response) {
List<ArticleBean.DataBean.DatasBean> datas = response.body().getData().getDatas();
callback.onResult(datas,page,page+1);
}
@Override
public void onFailure(Call<ArticleBean> call, Throwable t) {
}
});
}
@Override
public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, ArticleBean.DataBean.DatasBean> callback) {
}
@Override
public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, ArticleBean.DataBean.DatasBean> callback) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Constant.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
IMainService iMainService = retrofit.create(IMainService.class);
//next page key
Call<ArticleBean> call = iMainService.getArticle(params.key);
call.enqueue(new Callback<ArticleBean>() {
@Override
public void onResponse(Call<ArticleBean> call, Response<ArticleBean> response) {
List<ArticleBean.DataBean.DatasBean> datas = response.body().getData().getDatas();
callback.onResult(datas,params.key + 1);
}
@Override
public void onFailure(Call<ArticleBean> call, Throwable t) {
}
});
}
}
DataSource
的来源就是通过网络请求得到的数据。
PageKeyedDataSource<Integer, ArticleBean.DataBean.DatasBean>
// Key : Integer这里指的是页码
// Value : ArticleBean.DataBean.DatasBean 列表加载的List数据
-----loadInitial( LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, ArticleBean.DataBean.DatasBean> callback)
----初次加载数据,params:保存页码
callback.onResult(datas,page,page+1);
// datas:要显示的数据
// page:当前的页码数据
// page + 1:下一页的页码数 nextPageKey 保存在params中
------loadAfter
:加载分页数据
// params中保存的就是nextPageKey
Call<ArticleBean> call = iMainService.getArticle(params.key);
call.enqueue(new Callback<ArticleBean>() {
@Override
public void onResponse(Call<ArticleBean> call, Response<ArticleBean> response) {
List<ArticleBean.DataBean.DatasBean> datas = response.body().getData().getDatas();
//传递数据,更新params中的页码数据 +1
callback.onResult(datas,params.key + 1);
//再等下一次加载数据就是 params.key + 1 重新赋值
}
(2)DataSource.Factory
得到数据源之后,就需要通过工厂来创建DataSource.
public class MyPagingFactory extends DataSource.Factory {
private final int page;
private MyPagingSource2 pagingSource;
public MyPagingFactory(int page,MyPagingSource2 pagingSource){
this.page = page;
this.pagingSource = pagingSource;
}
@NonNull
@Override
public DataSource create() {
return pagingSource;
}
}
(3)创建PageList数据
public class MainViewModel extends AndroidViewModel {
private MyPagingSource2 pagingSource;
private LiveData<PagedList<ArticleBean.DataBean.DatasBean>> pagedListLiveData;
public MainViewModel(@NonNull Application application) {
super(application);
}
public LiveData<PagedList<ArticleBean.DataBean.DatasBean>> getPagedListLiveData(int page) {
PagedList.Config myPagingConfig = new PagedList.Config.Builder()
.setPageSize(10)
.setPrefetchDistance(2)
.setEnablePlaceholders(true)
.build();
pagingSource = new MyPagingSource2(page);
pagedListLiveData = new LivePagedListBuilder(
new MyPagingFactory(page,pagingSource),myPagingConfig
).build();
return pagedListLiveData;
}
public void invalidateDataSource(){
//刷新数据
pagingSource.invalidate();
}
}
PaList
数据的创建是通过LivePagedListBuilder
实现的,他需要两个值,DataSource.Factory
和PageList.Config
public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull PagedList.Config config) {
//noinspection ConstantConditions
if (config == null) {
throw new IllegalArgumentException("PagedList.Config must be provided");
}
//noinspection ConstantConditions
if (dataSourceFactory == null) {
throw new IllegalArgumentException("DataSource.Factory must be provided");
}
mDataSourceFactory = dataSourceFactory;
mConfig = config;
}
说一下PageList.Config
,他是对于分页加载数据显示的配置,常见的参数有:
setPageSize
:1页显示的数据
setPrefetchDistance(int count)
:当1页显示的数据还剩count个的时候,加载下一页的数据
setEnablePlaceholders
:是否启用占位符
如果您希望界面在应用完成数据获取前显示列表,可以向用户显示占位符列表项。PagedList 对这种情况的处理方式是将列表项数据显示为 null,直到加载了数据为止。
注意:默认情况下,分页库支持这种占位符行为。 占位符具有以下优点:
支持滚动条:PagedList 可向 PagedListAdapter
提供列表项数量。此信息允许适配器绘制滚动条来传达整个列表大小。有新页面载入时,滚动条不会跳到指定位置,因为列表不会改变大小。
无需加载旋转图标:由于列表大小已知,因此无需提醒用户正在加载更多项。占位符本身会传达这一信息。
(4)显示数据
创建了LiveData<PageList>
数据之后,需要在界面订阅该数据,然后加载适配器数据。
public class PageListAdapter extends PagedListAdapter<ArticleBean.DataBean.DatasBean, PageListAdapter.PagedViewHolder> {
protected PageListAdapter() {
super(new DiffUtil.ItemCallback<ArticleBean.DataBean.DatasBean>() {
@Override
public boolean areItemsTheSame(@NonNull ArticleBean.DataBean.DatasBean oldItem, @NonNull ArticleBean.DataBean.DatasBean newItem) {
return oldItem.getId() == newItem.getId();
}
@SuppressLint("DiffUtilEquals")
@Override
public boolean areContentsTheSame(@NonNull ArticleBean.DataBean.DatasBean oldItem, @NonNull ArticleBean.DataBean.DatasBean newItem) {
return oldItem.equals(newItem);
}
});
}
@NonNull
@Override
public PagedViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pae, parent, false);
return new PagedViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull PagedViewHolder holder, int position) {
String title = getItem(position).getTitle();
holder.tv_title.setText(title);
listener.onChanged(getItem(position).getNiceDate());
}
class PagedViewHolder extends RecyclerView.ViewHolder{
private TextView tv_title;
public PagedViewHolder(@NonNull View itemView) {
super(itemView);
tv_title = itemView.findViewById(R.id.tv_title);
}
}
public interface onPageChangedListener{
void onChanged(String time);
}
private onPageChangedListener listener;
public void setOnPageChangedListener(onPageChangedListener listener) {
this.listener = listener;
}
}
//page在这里传入,就相当于在DataSource处给Page赋值初始化
viewModel.getPagedListLiveData(0).observe(this, new Observer<PagedList<ArticleBean.DataBean.DatasBean>>() {
@Override
public void onChanged(PagedList<ArticleBean.DataBean.DatasBean> datasBeans) {
Log.e("TAG","datas==="+datasBeans);
//调用submitList传递数据。
adapter.submitList(datasBeans);
}
});
这样就实现了数据的分页加载;在之前讲过Room操作SQLite数据库,这个方式更简单,官方的说明文档就是以SQLite数据库为例,它简单在于执行sql查询语句的时候就可以直接返回一个DataSource.Factory,不需要去自定义,网上的文章比比皆是,但是网络操作的就很少,所以在这里先分享一部分。