Paging按页获取网络数据实例

许多应用程序可以处理大量数据,但只需要随时加载和显示一小部分数据。一个应用程序可能有数千个可能显示的项目,但它可能只需要一次访问几十个项目。如果应用程序不小心,它可能最终会请求它实际上不需要的数据,从而给设备和网络带来性能负担。如果数据与远程数据库存储或同步,则这也会降低应用速度并浪费用户的数据计划。

而谷歌推出的paging library 可以让 app 进行大数据查询的时候,在不过多增加设备负担或者等待时间的情况下,让渐进的从数据源加载数据变得更加简单。

官方文档:https://developer.android.com/topic/libraries/architecture/paging.html#java

中文版:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0920/8533.html

网上可以搜到很多介绍Paging框架的文章,可是真正介绍其怎么使用的却是少之又少,今天就给大家展示一下如何使用Paging分页从网络中加载数据。

谷歌官方提供的paging使用Demo:https://github.com/googlesamples/android-architecture-components

本文参考一:https://blog.csdn.net/alphathink/article/details/83380739

本文参考二:https://juejin.im/post/5a066be36fb9a045167ca8c7

本次提供的是java版本的Android Paging Library按页获取网络数据实例,如果想学习kotlin版本的Android Paging Library按页获取网络数据实例,请参考http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0922/8539.html

1.添加依赖

    我们来到maven中的paging库https://mvnrepository.com/artifact/android.arch.paging

三个库的区别在于:

序号 	名称 	    描述
1 	Runtime     是通过普通方式 使用paging
2 	Common 	    也是通过普通方式使用paging但是不提供测试依赖,官方这样描述:alternatively - without Android dependencies for testing
3 	RxJava2     是通过RxJava2使用paging

0.添加依赖 找到paging的依赖方式

在app的build.gradle中添加如下依赖

implementation 'android.arch.paging:runtime:1.0.0'

我添加1.0.1的依赖时提示下载失败,因此暂且用1.0.0版本

而paging需要标准的lifecycle依赖,因此还需要加上

annotationProcessor "android.arch.lifecycle:compiler:1.0.0"

为了大家对使用paging的整个流程有个清晰的认识,我们先放一张官网的流程图

1.DataSource 数据的来源

    Paging提供了三种不同的分页方式

Paging分页方式

名称 	描述

PageKeyedDataSource
	通常使用在论坛,贴吧中。根据传入的页面num进行获取某一页的数据,比如获取第10页。

PositionalDataSource
	Android开发中经常使用的分页,例如,每页10条,默认显示第一页。 CONTENT_LENGTH=10、mPage=1。

ItemKeyedDataSource
	使用上一条data对象里某一个值作为下一页请求的关键字。这种情况多出现在服务端已经完成了分页效果,在某一个对象添加如“下一页”这样的标识数据。

我们选择PositionalDataSource来构造我们的DataSource,并没有什么需要理解的东西,照着写就行

public class PageDataSourceFactory extends DataSource.Factory {
    public PositionalDataSource<RawBean> mPositionalDataSource;
    public PageDataSourceFactory(PositionalDataSource<RawBean> positionalDataSource) {
        this.mPositionalDataSource = positionalDataSource;
    }

    @Override
    public DataSource create() {
        return mPositionalDataSource;
    }
}

其中的RawBean是我们从网络中加载的数据元(javabean,包含名字,id等信息),替换为你们自己的数据元就好。关于其具体结构我放在文章的最后面。

2.PagedList 配置取数据方式

我以注释的方式将每一步的作用及原理都写在代码中了

public class MyViewModel extends AndroidViewModel {
    //每页需要加载的数量
    private static final int NUM_PER_PAGE = 15;
    //第一页
    private static final int PAGE_FIRST = 1;
    //当前页码数
    private int mPage = PAGE_FIRST;
    //列表数据
    private LiveData<PagedList<RawBean>> mLiveData;
    //用来存储网络返回的信息类
    private ResultBean resultBean;
    //默认的构造方法
    public MyViewModel(@NonNull Application application) {
        super(application);
    }

    public LiveData<PagedList<RawBean>> getLiveData() {
        initPagedList();//初始化PagedList
        return mLiveData;
    }

    /**
     * 初始化PageList
     */
    private void initPagedList() {
        final PositionalDataSource<RawBean> positionalDataSource = new PositionalDataSource<RawBean>() {
            List<RawBean> list = new ArrayList<>();

            /**
             * recyclerView第一次加载时自动调用
             * @param params 包含当前加载的位置position、下一页加载的长度count
             * @param callback 将数据回调给UI界面使用callback.onResult
             */
            @Override
            public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<RawBean> callback) {
                //计算显示到第几条数据
                final int position = computeInitialLoadPosition(params, NUM_PER_PAGE);
                //recyclerView第一次加载时我们调用OkHttp进行数据的加载
                //add后面都是post请求所需要的字段及值,根据请求数据的不同有所改变
                RequestBody body = new FormBody.Builder()
                        .add("searchValue","最好的我们")
                        .add("rows","10")
                        .add("page",String.valueOf(mPage))//1
                        .build();
                Request request = new Request.Builder()
                        .url(Constant.mainUrl + Constant.srearchUrl)
                        .post(body)
                        .build();
                try {
                    Response response = HttpUtils.getClient().newCall(request).execute();
                    String strResponse = response.body().string();
                    //使用Gson将返回的json数据解析为javabean
                    Gson gson = new Gson();
                    //从javabean中取出我们需要的bean
                    resultBean = gson.fromJson(strResponse,new TypeToken<ResultBean>(){}.getType());
                    list.addAll(resultBean.getRows());
                    //最重要的一步,paging是基于观察者模式,我们在这里调用callback.onResult();
                    //会直接将数据list返回到UI层,等下面接受到这个list数据的数据的时候我会提醒大家
                    //如果设置占位符需要调用三个参数的onResult()方法,最后一个参数为每页的总数据量
                    //我们没有设置占位符,因此调用两个参数的方法
                    callback.onResult(list,position);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            /**
             * 当用户滑动recyclerView到下一屏的时候自动调用,这里我们自动加载下一页的数据
             * @param params 包含当前加载的位置position、下一页加载的长度count
             * @param callback 将数据回调给UI界面使用callback.onResult
             */
            @Override
            public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<RawBean> callback) {
                //每次加载到分页条目时页数变为下一页
                mPage++;
                //使用OkHttp加载下一页数据
                RequestBody body = new FormBody.Builder()
                        .add("searchValue","最好的我们")
                        .add("rows","10")
                        .add("page",String.valueOf(mPage))
                        .build();
                Request request = new Request.Builder()
                        .url(Constant.mainUrl + Constant.srearchUrl)
                        .post(body)
                        .build();
                try {
                    Response response = HttpUtils.getClient().newCall(request).execute();
                    String strResponse = response.body().string();
                    //使用Gson解析数据
                    Gson gson = new Gson();
                    resultBean = gson.fromJson(strResponse,new TypeToken<ResultBean>(){}.getType());
                    list.addAll(resultBean.getRows());
                    callback.onResult(list);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };

        // 构建LiveData
        mLiveData = new LivePagedListBuilder(new PageDataSourceFactory(positionalDataSource)//自己定义
                , new PagedList.Config.Builder().setPageSize(NUM_PER_PAGE)//每次加载的数据数量15
                //距离本页数据几个时候开始加载下一页数据(例如现在加载10个数据,设置prefetchDistance为2,则滑到第八个数据时候开始加载下一页数据).
                .setPrefetchDistance(NUM_PER_PAGE)//15
                //这里设置是否设置PagedList中的占位符,如果设置为true,我们的数据数量必须固定,由于网络数据数量不固定,所以设置false.
                .setEnablePlaceholders(false).setInitialLoadSizeHint(NUM_PER_PAGE)//15
                .build()).build();

    }
}

3.PagedListAdapter 数据和UI配合的适配器

稍微了解Paging的小伙伴肯定都知道它是配合RecyclerView使用的,这里我们要继承PagedListAdapter,和RecyclerView.Adapter使用方式类似。

首先实现我们的ViewHolder继承RecyclerView.ViewHolder,因为findViewByid()比较耗时,因此我们在ViewHolder的构造方法中获取控件实例,并将其缓存在其中。

    class ViewHolder extends RecyclerView.ViewHolder{

        TextView tvResourceName;
        TextView tvResourceMagnet;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            tvResourceName = itemView.findViewById(R.id.resource_name);
            tvResourceMagnet = itemView.findViewById(R.id.resource_magnet);
        }
    }

list_item.xml中的代码如下,主要是展示电影的名称和磁力链接

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="4dp">

    <TextView
        android:id="@+id/resource_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="start"
        android:textColor="#000000"
        android:paddingTop="4dp"
        android:paddingBottom="4dp"
        android:textSize="16sp"
        android:textStyle="bold"
        android:text="名称"/>

    <TextView
        android:id="@+id/resource_magnet"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="start"
        android:paddingBottom="4dp"
        android:layout_marginLeft="20dp"
        android:text="magnet:?xt=urn:sha1:AAAAAAAAAAAAAAAAAAAAAAAA"/>

</LinearLayout>

   实现ViewHolder之后就要实现onCreateViewHolder()和onBindViewHolder()这两个方法,相信使用过recyclerView的小伙伴对这两个方法都非常熟悉,我在此就不详细介绍了。

    重点就是构造方法,我们在调用的时候要实现一个callback。也许大家会很奇怪,怎么找了半天也找不到这个适配器的数据源在哪?其实数据源的加载就是利用这个callback和第一步实现的MyViewModel共同实现的。

public class MyPagedListAdapter extends PagedListAdapter<RawBean,MyPagedListAdapter.MyViewHolder> {

    public MyPagedListAdapter(@NonNull DiffUtil.ItemCallback<RawBean> diffCallback) {
        super(diffCallback);
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false);
        MyViewHolder holder = new MyViewHolder(view);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ClipboardManager cm = (ClipboardManager) parent.getContext()
                        .getSystemService(Context.CLIPBOARD_SERVICE);
                ClipData mClipData = ClipData.newPlainText("Label",
                        "magnet:?xt=urn:sha1:" + getItem(position).getId());
                cm.setPrimaryClip(mClipData);
                Toast.makeText(parent.getContext(),"复制成功",Toast.LENGTH_SHORT).show();
            }
        });
        return holder;
    }



    @Override
    public void onBindViewHolder(@NonNull MyViewHolder baseViewHolder, int position) {
        RawBean bean = getItem(position);
        baseViewHolder.tvResourceName.setText(bean.getHighLightName());
        baseViewHolder.tvResourceMagnet.setText("magnet:?xt=urn:sha1:" + bean.getId());
    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        TextView tvResourceName;
        TextView tvResourceMagnet;

        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            tvResourceName = itemView.findViewById(R.id.resource_name);
            tvResourceMagnet = itemView.findViewById(R.id.resource_magnet);
        }
    }

}

4.UI展示

在xml布局中放置一个recyclerView

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_search_result"
        app:layout_constraintTop_toBottomOf="@id/ll_top_search"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

然后在java文件中获取其实例

private RecyclerView lvSearchResult;
lvSearchResult = view.findViewById(R.id.rv_search_result);
LinearLayoutManager manager = new LinearLayoutManager(mContext);
 lvSearchResult.setLayoutManager(manager);

下面adapter的实现和平时不太一样,其中重写的两个方法主要是为了判断

        private MyPagedListAdapter adapter;   

           adapter = new MyPagedListAdapter(new DiffUtil.ItemCallback<RawBean>(){
            /**
             *
             * @param rawBean rawBean The item in the old list.
             * @param t1 t1 The item in the new list.
             * @return True if the two items represent the same object or false if they are different.
             */
            @Override
            public boolean areItemsTheSame(@NonNull RawBean rawBean, @NonNull RawBean t1) {
                return rawBean.getId().equals(t1.getId());
            }

            /**
             *
             * @param rawBean rawBean The item in the old list.
             * @param t1 t1 The item in the new list
             * @return True if the contents of the items are the same or false if they are different.
             */
            @Override
            public boolean areContentsTheSame(@NonNull RawBean rawBean, @NonNull RawBean t1) {
                return rawBean.equals(t1);
            }
        });
       lvSearchResult.setAdapter(adapter);

    你以为已经可以成功显示数据了?不可能的。我们的数据源还没有设置呢。

    private MyViewModel myViewModel;
       //官网写法MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        //但是我找不到这个ViewModelProviders
        //将但是我找不到这个ViewModelProviders变成ViewModelProvider又没有of()方法,因此用下面是方法来实现
        myViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(getActivity().getApplication()).create(MyViewModel.class);

        myViewModel.getLiveData().observe(this, new Observer<PagedList<RawBean>>() {
            @Override
            public void onChanged(@Nullable PagedList<RawBean> rawBeans) {
                //调用的submit方法就是PagedListAdapter中的方法。它来对数据进行更新显示。
                adapter.submitList(rawBeans);
            }
        });

    这个submitList是最大的两点,adapte中的数据元就是从这里加载的。而且在MyViewModel中调用callback.onResult()方法中的list也是传到这了。怎么样?是不是恍然大悟了。

最后跟大家详细讲解一下我们的是数据源。

通过OkHttp请求,返回的数据格式如下:

   使用过Gson解析Json数据的小伙伴一定对这个javabean的结构了如执掌了吧。类中的属性名要和json中的标签名一直,其次每个javabean中都要有getter、setter方法。不同的小伙伴请自行搜索如何利用Gson解析Json数据。

javabean和json标签的对应关系

序号javabeanjson标签
1ResultBeanjson总标签
2List<RawBean>rows
3RawBeanrows中的每一组数据
4  
Paging按页获取网络数据实例
滚动到顶部