Android Jetpack架构组件-Paging自定义上拉加载更多

在前面章节介绍了Jetpack中的Paging的基本使用,在阅读本文前,若不知Paging的基本使用的朋友,可以查看笔者之前的文章Android Jetpack架构组件-Paging介绍及实践

知道了Paging的基本使用,但并不满足实际开发,虽然Paging可以实现分页加载,但Paging在数据请求的时,只要有一次返回的数据为空及PagedList为空,则再不会进行分页

这显然是不友好的,因为返回数据为空有多种原因,可能是网络或者查询数据格式等,返回的PageList为空,这个时候如果将分页结束掉,则显然不能接受;

或者Paging实现的分页加载,如果滑动很快的话,则会出现加载明显卡顿的效果,且无任何友好UI效果展示,如下图所示:


卡顿加载更多.gif

在实际开发中,我们希望是慢慢滑动的时候,Paging帮我们处理分页逻辑,而当快速滑动的时候,我们自己接管Paging的分页加载逻辑,出现加载更多的loading,如下效果所示:

加载更多有自定义加载动画

接下来,按照上面需求,实现当正常慢慢活动的时候,Paging帮我们分页,当快速滑动的时候,则我们接管Paging的分页加载,

示例以Jetpack中的
ViewModelDataSourcePagingPagingListAdapter并且配合SmartRefreshLayout来完成上拉加载和下拉刷新

  • 当然监听RecycleView加载更多的视图的有很多种方法,这里直接使用SmartRefreshLayout

在开始之前,先通过ViewModel+DataSource+PagingListAdapter将数据绑定到RecycleView上,若看过之前的基本使用,则以下基本使用部分可以略过

Paging的基本使用

  • 1、先将Paging的基本使用及数据加载完成,则Activity中的代码如下所示:
package com.onexzgj.inspur.pageingsample.pagingpro;

public class PagingProActivity extends AppCompatActivity implements OnRefreshListener, OnLoadMoreListener {

    @SuppressLint("RestrictedApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_paging_pro);

        recyclerView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));

        adapter = new PagingProAdapter(this);
        recyclerView.setAdapter(adapter);

        paingProViewModel = new ViewModelProvider.NewInstanceFactory().create(PaingProViewModel.class);

        paingProViewModel.getPageData().observe(this, new Observer<PagedList<ResponseArticle.DataBean.Article>>() {
            @Override
            public void onChanged(PagedList<ResponseArticle.DataBean.Article> articles) {
                submitList(articles);
            }
        });
    }

    public void submitList(PagedList<ResponseArticle.DataBean.Article> result) {
        if (result.size() > 0) {
            adapter.submitList(result);
        }
    }
}
  • 2、再来看看PaingProViewModel中的实现
package com.onexzgj.inspur.pageingsample.pagingpro;
/**
 * author:onexzgj
 * time:2020/5/4
 */
public class PaingProViewModel extends AbsPagingProViewModel<ResponseArticle.DataBean.Article> {
    private AtomicBoolean loadAfter = new AtomicBoolean(false);
    private int mPageIndex = 0;

    public int getmPageIndex() {
        return mPageIndex;
    }

    @Override
    protected DataSource createDataSource() {
        return new ArticleDataSource();
    }


    class ArticleDataSource extends PageKeyedDataSource<Integer, ResponseArticle.DataBean.Article> {

        @Override
        public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, ResponseArticle.DataBean.Article> callback) {
            loadData(0, callback, null);
        }

        @Override
        public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, ResponseArticle.DataBean.Article> callback) {
            callback.onResult(Collections.emptyList(), 0);
        }

        @Override
        public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, ResponseArticle.DataBean.Article> callback) {
            loadData(params.key, null, callback);
        }
    }


    //简单的请求网络业务逻辑
    @SuppressLint("RestrictedApi")
    private void loadData(int pageIndex, PageKeyedDataSource.LoadInitialCallback<Integer, ResponseArticle.DataBean.Article> initCallback, PageKeyedDataSource.LoadCallback<Integer, ResponseArticle.DataBean.Article> callback) {

        mPageIndex = pageIndex;
        if (pageIndex > 0) {
            loadAfter.set(true);
        }

        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url("https://www.wanandroid.com/article/list/" + pageIndex + "/json").build();
        try {
            Response response = null;
            response = client.newCall(request).execute();
            if (response.isSuccessful()) {
                ResponseArticle responseArticle = JSON.parseObject(response.body().string(), ResponseArticle.class);

                if (initCallback != null) {
                    initCallback.onResult(responseArticle.getData().getDatas(), pageIndex - 1, pageIndex + 1);
                } else {
                    callback.onResult(responseArticle.getData().getDatas(), pageIndex + 1);
                }

                if (pageIndex > 0) {
                    //通过BoundaryPageData发送数据 告诉UI层 是否应该主动关闭上拉加载分页的动画
                    ((MutableLiveData) getBoundaryPageData()).postValue(responseArticle.getData().getDatas().size() > 0);
                    loadAfter.set(false);
                }
                mPageIndex = pageIndex + 1;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 3、通过PagedListAdapter将数据绑定到RecycleView上
import com.onexzgj.inspur.pageingsample.R;
/**
 * author:onexzgj
 * time:2020/5/4
 */
public class PagingProAdapter extends PagedListAdapter<ResponseArticle.DataBean.Article, PagingProAdapter.ViewHolder> {
    public Context mContext;

    protected PagingProAdapter(Context context) {
        super(new DiffUtil.ItemCallback<ResponseArticle.DataBean.Article>() {

            @Override
            public boolean areItemsTheSame(@NonNull ResponseArticle.DataBean.Article oldItem, @NonNull ResponseArticle.DataBean.Article newItem) {
                return oldItem == newItem;
            }

            @Override
            public boolean areContentsTheSame(@NonNull ResponseArticle.DataBean.Article oldItem, @NonNull ResponseArticle.DataBean.Article newItem) {
                return oldItem.getId() == newItem.getId();
            }
        });
        this.mContext= context;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(mContext).inflate(R.layout.item, parent, false);
        return new ViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.bindData(getItem(position));
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        private TextView nameView;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            nameView = itemView.findViewById(R.id.tv_info);
        }

        public void bindData(ResponseArticle.DataBean.Article item) {
            nameView.setText(item.getTitle());
        }
    }
}

到这里Paging的基本使用则已经完成,接下来,我们将实现手动接管Paging的上拉加载与下来刷新

实现上拉加载和下拉刷新

  • 1、通过SmartRefreshLayout,来监听RecycleView的下拉刷新与上拉加载更多的监听,如何使用SmartRefreshLayout这里不做详述
        ...
        smartRefreshLayout.setEnableRefresh(true);
        smartRefreshLayout.setEnableLoadMore(true);
        smartRefreshLayout.setOnRefreshListener(this);
        smartRefreshLayout.setOnLoadMoreListener(this);
        ...
  • 2、刷新逻辑实现
    通过实现smartRefreshLayout的onRefresh(),将DataSource重新初始化一下即可,即如下所示:
    @Override
    public void onRefresh(@NonNull RefreshLayout refreshLayout) {
        paingProViewModel.getDataSource().invalidate();
    }
  • 3、 加载更多逻辑实现

通过实现smartRefreshLayout的loadMore()中的实现逻辑,如下所示:

    @Override
    public void onLoadMore(@NonNull RefreshLayout refreshLayout) {

        //若列表数据为空,则不触发上拉加载更多数据
        final PagedList<ResponseArticle.DataBean.Article> currentList = adapter.getCurrentList();
        if (currentList == null || currentList.size() <= 0) {
            finishRefresh(false);
            return;
        }

        //需要注意这里,在PaingProViewModel中自实现loadAfter方法,实现请求分页数据的逻辑
        paingProViewModel.loadAfter(paingProViewModel.getmPageIndex(),new PageKeyedDataSource.LoadCallback<Integer, ResponseArticle.DataBean.Article>(){

            @Override
            public void onResult(@NonNull List<ResponseArticle.DataBean.Article> data, @Nullable Integer adjacentPageKey) {
                PagedList.Config config = currentList.getConfig();
                if (data != null && data.size() > 0) {
                    //这里 咱们手动接管 分页数据加载的时候 使用MutableItemKeyedDataSource也是可以的。
                    //由于当且仅当 paging不再帮我们分页的时候,我们才会接管。所以 就不需要ViewModel中创建的DataSource继续工作了,所以使用新的DataSource对象,这里是MutablePageKeyedDataSource
                    MutablePageKeyedDataSource dataSource = new MutablePageKeyedDataSource();

                    //这里要把列表上已经显示的先添加到dataSource.data中
                    //而后把本次分页回来的数据再添加到dataSource.data中
                    dataSource.data.addAll(currentList);
                    dataSource.data.addAll(data);

                    PagedList pagedList = dataSource.buildNewPagedList(config);
                    submitList(pagedList);
                }
            }
        });
    }

可以看到我们通过,在PaingProViewModel中定义loadAfter方法,实现接管Paging分页加载的请求数据逻辑,

  • 4、实现PaingProViewModel中的自定义的方法loadAfter()
    @SuppressLint("RestrictedApi")
    public void loadAfter(int pageIndex, PageKeyedDataSource.LoadCallback<Integer, ResponseArticle.DataBean.Article> callback) {

        Log.d("TAG", "loadAfter: pageIndex" + pageIndex);
        //是否加载更多的表示位
        if (loadAfter.get()) {
            callback.onResult(Collections.emptyList(), 0);
            return;
        }

        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url("https://www.wanandroid.com/article/list/" + pageIndex + "/json").build();
        ArchTaskExecutor.getIOThreadExecutor().
                execute(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    Response response = null;
                                    response = client.newCall(request).execute();
                                    if (response.isSuccessful()) {
                                        ResponseArticle responseArticle = JSON.parseObject(response.body().string(), ResponseArticle.class);
                                        callback.onResult(responseArticle.getData().getDatas(), pageIndex + 1);

                                        if (pageIndex > 0) {
                                            //通过BoundaryPageData发送数据 告诉UI层 是否应该主动关闭上拉加载分页的动画
                                            ((MutableLiveData) getBoundaryPageData()).postValue(responseArticle.getData().getDatas().size() > 0);
                                            loadAfter.set(false);
                                        }
                                        mPageIndex = pageIndex + 1;
                                    }
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                );
    }

loadAfter为设置是否是Paging上拉加载的标记位,只有Paging进行过上拉加载的时候,才接管上拉加载,即加载的页码大于0的时候才接管,否则返回空的PagedList即可。

  • 5、自定义的MutablePageKeyedDataSource的实现
package com.onexzgj.inspur.pageingsample.pagingpro;

@SuppressLint("RestrictedApi")
public class MutablePageKeyedDataSource<Value> extends PageKeyedDataSource<Integer, Value> {
    public List<Value> data = new ArrayList<>();

    public PagedList<Value> buildNewPagedList(PagedList.Config config) {
      PagedList<Value> pagedList = new PagedList.Builder<Integer, Value>(this, config)
                .setFetchExecutor(ArchTaskExecutor.getIOThreadExecutor())
                .setNotifyExecutor(ArchTaskExecutor.getMainThreadExecutor())
                .build();

        return pagedList;
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, Value> callback) {
        callback.onResult(data, null, null);
    }

    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, Value> callback) {
        callback.onResult(Collections.emptyList(), null);
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, Value> callback) {
        callback.onResult(Collections.emptyList(), null);
    }
}

作用相当于重新创建一个新的DataSource,且绑定数据集合构建出一个PagedList对象,供Paging使用。

总结

到这里,Paging自定义上拉加载更多介绍完了,建档总结,即通过SmartRefreshLayout监听RecycleView的loadMore方法,通过在ViewModel中自定义loadAfter来加载数据,且重新创建DataSource和将集合数据List,和重新构建出一个PageList即可,文章中的示例代码已上Jetpack/pagingpro

该仓库为演示Jetpack的组件的仓库,分别对Lifecyele、LiveData、ViewModel、Room、WorkManager、Paging的介绍和使用

详细介绍文章

项目目录结构为如下

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260