基于Jetpack之Paging控件分页加载瀑布式列表

篇章目标要点

Paging是Jetpack推出的分页加载组件,可以应用于分批次从网络/本地数据库请求数据,实现分批加载显示的功能。本文结合Room本地数据库展示Paging分页展示的功能实现过程,以及部分原理的说明

实现效果

  1. 列表向下滑动快至底部时,可以自动加载下一页数据,效果如下
    在这里插入图片描述

  2. 分页加载的log如下
    在这里插入图片描述

基本架构描述

基于MVVM架构设计下的Paging控件的基本架构如下,其主要控件的功能说明如下。
PagedListAdapter : 其中RecyclerView的Adapter需要实现此类,其内部需要基于DiffUtil差分组件处理列表数据的差分更新
PagedList.Config : 设置分页加载的配置参数,包括每页的列表大小,预加载的长度,剩余多少时自动加载邻近页数据
PageKeyDataSource : 需要继承此类实现分页逻辑,当中需要处理加载初始数据,加载下一页数据,加载上一页数据的逻辑。当中注意处理页数的调整,以及页数到顶和到底时避免加载重复数据。
DataSource.Factory : 创建DataSource类

在这里插入图片描述

Paging主要代码说明

View层的数据监听和更新

通过LiveData监听列表数据变化后,更新至PagedListAdapter

//监听列表数据变化
mViewModel.getPagedListLiveData().observe(this, new Observer<PagedList<Picture>>() {
    @Override
    public void onChanged(PagedList<Picture> pageList) {
        mAdapter.submitList(pageList);
        Log.d(TAG,"size = "+pageList.size());
    }
});

PagedListAdapter的实现过程

要注意的是在onBindViewHolder方法中获取位置相应的数据时,务必要使用getItem(position)方法获取位置的数据,因为该方法逻辑当中会自动加载邻近页的数据。

/**
 * Paging控件Adapter基类
 * @author mailanglideguozhe
 * @date 20210621
 * @param <T>
 */
public abstract class BaseAdapter<T> extends PagedListAdapter<T , BaseAdapter.ViewHolder> {
    private static final String TAG = "BaseAdapter";
    private List<T> dataList;

    public BaseAdapter() {
        super(DIFF_CALLBACK);
    }

    public void setDataList(List<T> dataList) {
        this.dataList = dataList;
        notifyDataSetChanged();
    }

    public List<T> getDataList() {
        return dataList;
    }

    @NonNull
    @Override
    public BaseAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(getLayoutId(),null);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull BaseAdapter.ViewHolder holder, int position) {
        if(null != dataList && null != dataList.get(position)){
            setViewData(holder , position, getItem(position));
        }
        holder.itemView.setTag(position);
    }

    @Override
    public void onBindViewHolder(@NonNull BaseAdapter.ViewHolder holder, int position, @NonNull List<Object> payloads) {
        super.onBindViewHolder(holder, position, payloads);
        setViewData(holder , position, getItem(position));
    }
    
    //由实现类完成视图的初始化和赋值
    public abstract void setViewData(ViewHolder holder , int pos, T t);

    //由实现类指定布局
    public abstract int getLayoutId();

    /**
     * 对象数据的比较
     */
    private static DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback<Picture>() {
        @Override
        public boolean areItemsTheSame(@NonNull Picture oldItem, @NonNull Picture newItem) {
            Log.d(TAG,"areItemsTheSame = "+(oldItem.id ==  newItem.id));
            return oldItem.id ==  newItem.id;
        }

        @Override
        public boolean areContentsTheSame(@NonNull Picture oldItem, @NonNull Picture newItem) {
            Log.d(TAG,"areContentsTheSame = "+oldItem.name.equals(newItem.name));
            return oldItem.name.equals(newItem.name);
        }
    };

    public class ViewHolder extends RecyclerView.ViewHolder{
        public ImageView imageView;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.imageview);
        }
    }
}

ViewModel实现注意事项

当中会完成分页控件的配置和创建PagedList的Livedata监听数据

/**
 * 基于PagedList获取数据
 * @author mailanglideguozhe
 * @date 20210710
 */
public class PictureViewModel extends ViewModel {
    private static final String TAG = "PictureViewModel";
    //监听当前列表数据
    private LiveData<PagedList<Picture>> mPagedListLiveData;
    //数据库总长度监听
    private MutableLiveData<Integer> mCountLiveData = new MutableLiveData<>();
    public PictureViewModel() {
        //设置分页控件的配置参数
        PagedList.Config config = new PagedList.Config.Builder()
                .setPageSize(Constants.PAGE_SIZE)//每页大小
                .setPrefetchDistance(4)//设置距离底部还有多少条数据时开始加载下一页
                .setEnablePlaceholders(true)//设置占位
                .setInitialLoadSizeHint(Constants.PAGE_SIZE *2)//设置首次加载的长度
                .build();
        mPagedListLiveData = new LivePagedListBuilder<>(new PageSourceFactory(), config).build();
    }

    public LiveData<PagedList<Picture>> getPagedListLiveData() {
        return mPagedListLiveData;
    }

    public MutableLiveData<Integer> getCountLiveData() {
        return mCountLiveData;
    }

    /**
     * 请求计算列表总长度
     */
    ……
}

PageKeyedDataSource分页加载逻辑

实现PageKeyedDataSource分页加载逻辑时,需要完成加载首页数据,前一页和后一页数据,如果页数已经置顶或者处于顶部时,请勿重复请求数据。

/**
 * 分页处理数据逻辑。注意数据加载需要在子线程进行
 * @author mailanglideguozhe
 * @date 2021-6-22
 */
public class PageSource extends PageKeyedDataSource<Integer , Picture> {
    private static final String TAG = "PageSource";
    /**
     * 页码从0开始
     */
    private int FIRST_PAGE = 0;

    /**
     * 加载首页的数据
     * @param params 加载的参数
     * @param callback 用于回调结果的callback
     */
    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, Picture> callback) {
        Log.d(TAG,"initial page = "+ FIRST_PAGE);
        Schedulers.computation().scheduleDirect(new Runnable() {
            @Override
            public void run() {
                PictureHelper pictureHelper = PictureHelper.getInstance();
                pictureHelper.getData(FIRST_PAGE *Constants.PAGE_SIZE,Constants.PAGE_SIZE,callback, FIRST_PAGE +1);
            }
        });

    }

    /**
     * 加载前一页数据,注意仅在有前一页数据时调用数据请求
     * @param params 加载的参数
     * @param callback 用于回调结果的callback
     */
    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, Picture> callback) {
        Log.d(TAG,"before page = "+ (params.key - 1));
        Schedulers.computation().scheduleDirect(new Runnable() {
            @Override
            public void run() {
                if(params.key > 0){
                    PictureHelper pictureHelper = PictureHelper.getInstance();
                    pictureHelper.getData(params.key, Constants.PAGE_SIZE,callback,params.key - 1);
                }
            }
        });
    }

    /**
     *加载下一页数据,注意仅在有下一页数据时调用数据请求
     * @param params 加载的参数
     * @param callback 用于回调结果的callback
     */
    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, Picture> callback) {
        Log.d(TAG,"after page = "+ params.key);
        FIRST_PAGE++;
        Schedulers.computation().scheduleDirect(new Runnable() {
            @Override
            public void run() {
                PictureHelper pictureHelper = PictureHelper.getInstance();
                if(pictureHelper.hasNextPage(params.key)){
                    int nextKey = params.key+1;
                    pictureHelper.getData(params.key, Constants.PAGE_SIZE,callback, nextKey);
                }
            }
        });
    }
}

DataSource.Factory实现逻辑

此处代码较少,主要就是创建DataSource

/**
 * 创建DataSource实现对象,并且包装之后暴露给ViewModel
 * @author mailangdeguozhe
 * @date 20220709
 */
public class PageSourceFactory extends DataSource.Factory<Integer , Picture> {
    private MutableLiveData<PageSource> mPictureLiveData = new MutableLiveData<>();

    @NonNull
    @Override
    public DataSource<Integer, Picture> create() {
        PageSource pageSource = new PageSource();
        mPictureLiveData.postValue(pageSource);
        return pageSource;
    }
}

至此Paging控件相关的主要代码均已经展示了,room数据库操作不是本文的重点,没有粘贴相应代码

瀑布流相关设置

使用StarggeredGridLayoutManager

该种布局下可以实现视图参差感

StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(2,RecyclerView.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
RecyclerView.ItemDecoration decoration = new StaggeredDecoration(5);
recyclerView.addItemDecoration(decoration);

Adapter中设置ImageView自适应宽高

根据网络图片的Bitmap宽高比重新设置ImageView的高度并重新布局,从而实现View高度差异化。以下方法是在onBindViewHolde()方法中调用

public void setViewData(ViewHolder holder, int pos, Picture picture) {
    if(null != picture){
        //实例化图片显示
        View view = holder.itemView;
        ImageView imageView = holder.imageView;
        ViewGroup.LayoutParams lp = imageView.getLayoutParams();
        Glide.with(view.getContext()).asBitmap().load(picture.url).placeholder(R.drawable.abc_vector_test).override(lp.width, lp.height).into(new SimpleTarget<Bitmap>() {
            @Override
            public void onResourceReady(@NonNull Bitmap bitmap, @Nullable Transition transition) {
                //根据bitmap的长宽比例,调整view的高度
                mRatio = bitmap.getHeight()* 1.00f / bitmap.getWidth();
                if(mRatio > 1.5f){
                    mRatio = 1.5f;
                }else if(mRatio < 0.5f){
                    mRatio = 0.5f;
                }
                Log.d(TAG, "mRatio = "+mRatio+",height = "+bitmap.getHeight()+",width = "+bitmap.getWidth());
                lp.width = lp.width;
                lp.height = (int)(lp.width * mRatio);
                imageView.setLayoutParams(lp);
                holder.itemView.requestLayout();
                imageView.setImageBitmap(bitmap);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            }
        });
    }
}

Item的Layout中设置ImageView属性

android:adjustViewBounds="true"

Paging控件使用的优劣性

劣势

先说下其不足点,主要了较多的相关类的实现,这个是标准RecyclerView所没有必要的类,比如PageKeyedDataSource实现, DataSource.Factory实现,ViewModel
中进行PagedList.Config定义。

优势

要展示的数据较多时,为了保证加载性能,不得不采用分页加载的数据,其优势体现在以下几个方面
1. 封装了自动加载下一页数据的逻辑。这部分免除开发人员书写相关代码,比如容易设置距离底部剩余多少数据时加载下一页数据
2. 页码序号加载管理便利,我们只需要在前一页和下一页时设定-1和+1即可
3. 配合网络分页加载api非常便利

学习心得

文中Demo给出了一个实现思路和基本功能完成,Paging控件存在其自身的优势和劣势,可以根据项目的需要选择是否采用。源码分享如下:

https://gitee.com/com_mailanglidegezhe/paging-example.git
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值