Android设计模式实战-对RV.Adapter进行装饰,实现addHead与LoadMore功能

1,498 阅读10分钟

我正在参与掘金创作者训练营第6期,点击了解活动详情

前言

RecyclerView 的使用可以说是开发中必备的技能,我们常常使用数据适配器RecyclerView.Adapter 的时候会觉得每次要写重复的代码,觉好麻烦。

通常大家都是引入一些第三方库,或自己做一些简单的封装,这里我分享一下自己的RV.Adapter的封装,使用装饰器在不改变原有功能的情况下给RV.Adapter添加一些新的功能。

大家可以对比一下自己封装,看看有没有什么启发,当然如果觉得我的封装不对或不好的地方也可以告知我。

OK,那我们开始吧!

一、对RV.Adapter的封装,添加数据处理

我们自己起名封装一个基类 BaseRVAdapter 是继承 RecyclerView.Adapter 实现的,由于是继承实现的,所以严格来说它不是装饰器,就是普通的通过继承扩展方法。

如果是装饰器的定义,那么应该是把 RecyclerView.Adapter 当构造传入,再对它进行一些功能的扩展。

能不能使用装饰器的方式来定义呢?当然是可以的,但是没有必要因为是基类的,必须选择的,常用的逻辑,我们为了方便其他人使用就用继承来实现必须的逻辑。

话不多说直接上代码:

/**
 * RecyclerView的基类Adapter的封装。兼容多布局(根据data判断哪种布局)。
 * 可以兼容第三方RecyclerView
 */
public abstract class BaseRVAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
    protected int mLayoutId;
    protected List<T> mDatas;
    protected Context mContext;
    private IHasMoreType<T> mIMoreType;

    /**
     * 普通的数据填充走此布局
     */
    public BaseRVAdapter(Context context, List<T> datas, int layoutId) {
        mLayoutId = layoutId;
        mDatas = datas;
        mContext = context;
    }

    /**
     * 普通的数据填充走此布局,不加入数据源,必须通过setDataList方法设置数据源
     */
    public BaseRVAdapter(Context context, int layoutId) {
        mLayoutId = layoutId;
        mContext = context;
    }

    /**
     * 设置数据源,只好在rv设置adapter之前调用
     */
    public void setDataList(List<T> datas) {
        mDatas = datas;
    }


    /**
     * 更新全部的数据源
     *
     * @param datas 更新的全部数据源
     */
    public void updateDataList(List<T> datas) {
        mDatas.clear();
        notifyDataSetChanged();
    }

    /**
     * 添加数据
     *
     * @param datas 需要添加的数据源
     */
    public void addDataList(List<T> datas) {
        if (mDatas != null) {
            mDatas.addAll(datas);
        }

        notifyItemRangeInserted(mDatas.size() - datas.size(), datas.size());
    }


    /**
     * 如果有多个布局走此构造方法
     */
    public BaseRVAdapter(Context context, List<T> datas, IHasMoreType<T> iMoreType) {
        mIMoreType = iMoreType;
        mDatas = datas;
        mContext = context;
    }

    /**
     * 多布局的获取布局类型
     */
    @Override
    public int getItemViewType(int position) {
        //如果支持多布局
        if (mIMoreType != null) {
            return mIMoreType.getLayoutId(mDatas.get(position));
        }
        return super.getItemViewType(position);
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //如果支持多布局
        if (mIMoreType != null) {
            mLayoutId = viewType;
        }
        return BaseViewHolder.get(mContext, parent, mLayoutId);
    }

    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        /**  抽象方法交由子类具体实现  **/
        bindData(holder, mDatas.get(position), position);
    }

    public abstract void bindData(BaseViewHolder holder, T t, int position);

    @Override
    public int getItemCount() {
        return mDatas == null ? 0 : mDatas.size();
    }
}

多类型的布局接口控制:

/**
 * 如果有多布局的控制接口,可以传入构造方法
 */
public interface IHasMoreType<T> {
    //根据当前item的数据,返回指定的布局id
    int getLayoutId(T t);
}

还有一个 BaseViewHolder 我没有贴出来,是很简单的代码,就是对一些TextView CheckBos 等控件定义一些SetText Color ClickListener等方法。有需要到文末查看源码自取。

本功能由Java语言实现,更方便在Java与Kotlin两种语言中使用,方法都已经注释的很详细。

如何使用与定义呢?简单的来看看:

定义数据与数据适配器,添加数据即可展示


    private val mDatas = mutableListOf<String>()
    private val mAdapter = JobAdapter(mDatas)

    override fun init() {

        mBinding.recyclerView.vertical().adapter = mAdapter

        initData()
    }

    private fun initData() {

        val newList = listOf("刘备", "关羽", "张飞", "赵云", "马超", "黄忠", "诸葛亮", "姜维", "王平", "魏延")
        mAdapter.addDataList(newList)

    }

简单的数据适配器:

class JobAdapter(datas: MutableList<String>) : BaseRVAdapter<String>(CommUtils.getContext(), datas, R.layout.item_custom_jobs) {

    override fun bindData(holder: BaseViewHolder, t: String?, position: Int) {

        holder.setText(R.id.tv_job_text, t)
    }
}

效果:

image.png

我们可以很方便的实现一个基本的列表功能,如果是多布局的类型,我们上面也进行了封装,实现我们定义好的 IHasMoreType 接口,然后通过数据类型判断返回的布局id。在赋值的时候我们根据数据类型对不同的布局做不同的绑定数据操作。

看看具体如何使用:

    private val mDatas = mutableListOf<MoreJob>()
    private val mAdapter = Job2Adapter(mDatas)

    private fun initData() {

       mBinding.recyclerView.vertical().adapter = mAdapter

        val newList = listOf(
            MoreJob(1, "刘备"),
            MoreJob(1, "张飞"),
            MoreJob(2, "曹操"),
            MoreJob(1, "赵云"),
            MoreJob(2, "曹仁"),
            MoreJob(1, "马超"),
            MoreJob(1, "黄忠"),
            MoreJob(1, "诸葛亮"),
            MoreJob(2, "夏侯惇"),
            MoreJob(1, "姜维"),
            MoreJob(1, "王平"),
            MoreJob(2, "徐晃"),
            MoreJob(1, "魏延"),
            MoreJob(2, "张辽"),
            MoreJob(2, "曹真"),
            MoreJob(1, "刘禅"),
            MoreJob(1, "张苞"),
            MoreJob(2, "司马懿"),
        )

        mAdapter.addDataList(newList)

    }

Adapter的多布局使用:

class Job2Adapter(datas: MutableList<MoreJob>) : BaseRVAdapter<MoreJob>(CommUtils.getContext(), datas,
    IHasMoreType<MoreJob> {
        if (it.type == 1) return@IHasMoreType R.layout.item_custom_jobs
        else return@IHasMoreType R.layout.item_custom_image
    }) {

    override fun bindData(holder: BaseViewHolder, bean: MoreJob, position: Int) {
        if (bean.type == 1) {
            holder.setText(R.id.tv_job_text, bean.name)
        } else {
            holder.setText(R.id.tv_name, bean.name)
        }
    }
    
}

效果:

image.png

二、对RV.Adapter的装饰,添加头脚布局

上文的 BaseRVAdapter 只是继承的实现,只是对数据处理,BaseViewHolder进行一些操作处理,并没有添加一些新的功能逻辑,只是对Adapter现有的功能进行整合与封装。

那么现在就不同了,我们现在就是定义一个装饰器,我们把 RecyclerView.Adapter 当构造传入,对它进行一些功能的扩展。如添加/删除头布局,添加/删除脚布局等新的功能逻辑。

代码如下:(PS:为什么贴图不放源码?代码太多了不方便观看,其实代码是开源的,如果有需求可以去文末源码中自取。)

image.png image.png image.png image.png image.png

我们对 RecyclerView.Adapter 现有的一些方法, onBindViewHolder getItemCount onCreateViewHolder 等方法做了一些处理,加入了一些头布局,脚布局的判断逻辑然后再调用 RecyclerView.Adapter 的处理。

其次,我们还添加了一些全新的方法,如 addHeadView addFootView removeHeadView removeFootView 等方法,用于扩展它的功能。

由于使用了装饰者模式,我们直接把原来的 RecyclerView.Adapter 当构造传入,包裹起来就能实现新的功能。

    private val mDatas = mutableListOf<MoreJob>()
    private val mAdapter = Job2Adapter(mDatas)
    private val wrapAdapter = WrapRVAdapter(mAdapter)

      override fun init() {
        wrapAdapter.addHeadView(CommUtils.inflate(R.layout.item_vertal_header))
        wrapAdapter.addFootView(CommUtils.inflate(R.layout.item_vertal_fooder))

        mBinding.recyclerView.vertical().adapter = wrapAdapter
    }

添加数据的逻辑没变,只是把之前的 Adapter 包裹了一次,就能实现添加头布局脚布局的逻辑。

添加了头布局和脚布局之后,效果如下:

image.png image.png

三、对RV.Adapter的装饰,添加LoadMore功能

在日常的开发中,添加头布局和添加脚布局相对都是还算比较少见的,并且可以使用其他的方式实现,例如嵌套滑动,Appbarlayout等,所以添加头布局和脚布局是非必须的。

但是一个列表的加载更多功能确是非常必要的,因为后端返回的接口是分页列表,我们App需要配合做分页展示,也就需要LoadMore功能,可以说是绝大部分项目都会用到的。

平常使用的过程中,我们需要滑动到底部添加LoadMore的回调,并暴露一些方法,设置LoadMore的状态,比如Loading中,加载错误,加载成功,加载到底了。

相信大家很多人都是使用的第三方库,又或者自己封装的,那接下来我们就看看如何使用装饰器来封装一个加载更多的Adapter吧,对照一下有没有什么不一样。

首先我们需要准备一个 LoadMoreView 对象,封装一些状态切换之类的工具方法。

然后我们就能封装一个 LoadMoreAdapter 装饰器,把 RecyclerView.Adapter 当做构造传参进去,然后对它现有方法进行一些改造,再添加一些新的方法扩展它的功能。

封装LoadMore的布局与状态切换:

/**
 * 自定义View 用于LoadMoreAdapter
 * RV中加载更多的布局
 */
public class LoadMoreView {

    public static final int STATUS_DEFAULT = 1;
    public static final int STATUS_LOADING = 2;
    public static final int STATUS_FAIL = 3;
    public static final int STATUS_END = 4;
    private int mLoadMoreStatus = STATUS_DEFAULT;

    //提供LoadMore的布局Id
    public int getLayoutId() {
        return R.layout.base_rv_loadmore_view;
    }

    public void setLoadMoreStatus(int loadMoreStatus) {
        this.mLoadMoreStatus = loadMoreStatus;
    }

    public int getLoadMoreStatus() {
        return mLoadMoreStatus;
    }

    public void showState(BaseViewHolder holder) {

        YYLogUtils.w("holder:" + holder + " mLoadMoreStatus:" + mLoadMoreStatus);

        if (mLoadMoreStatus == STATUS_DEFAULT) {
            visibleLoading(holder,false);
            visibleLoadFail(holder,false);
            visibleLoadEnd(holder,false);
        } else if (mLoadMoreStatus == STATUS_LOADING) {
            visibleLoading(holder,true);
            visibleLoadFail(holder,false);
            visibleLoadEnd(holder,false);
        } else if (mLoadMoreStatus == STATUS_FAIL) {
            visibleLoading(holder,false);
            visibleLoadFail(holder,true);
            visibleLoadEnd(holder,false);
        } else if (mLoadMoreStatus == STATUS_END) {
            visibleLoading(holder,false);
            visibleLoadFail(holder,false);
            visibleLoadEnd(holder,true);
        }
    }

    private void visibleLoading(BaseViewHolder holder, boolean visible) {
        holder.setVisible(R.id.load_more_loading_view, visible);
    }

    private void visibleLoadFail(BaseViewHolder holder, boolean visible) {
        holder.setVisible(R.id.load_more_load_fail_view, visible);
    }

    private void visibleLoadEnd(BaseViewHolder holder, boolean visible) {
        holder.setVisible(R.id.load_more_load_end_view, visible);
    }
}

加载更多的装饰器:(PS:为什么贴图不放源码?代码太多了不方便观看,其实代码是开源的,如果有需求可以去文末源码中自取。)

image.png image.png image.png image.png image.png

注释的很详细,代码不难,大家对LoadMore的实现有疑问的话可以评论区提问。

使用:

    private val mDatas = mutableListOf<MoreJob>()
    private val mAdapter = Job2Adapter(mDatas)
    private val mLoadMoreAdapter = LoadMoreAdapter(mAdapter)


    override fun init() {

        mLoadMoreAdapter.isLoadMoreEnable = true
        mLoadMoreAdapter.setPreLoadNumber(3)
        mLoadMoreAdapter.setOnLoadMoreListener {
            loadMoreData()
        }

        mBinding.recyclerView.vertical().adapter = mLoadMoreAdapter

        initData()
    }

    private fun loadMoreData() {
        //模拟网络请求
        CommUtils.getHandler().postDelayed({

            mAdapter.addDataList(
                listOf(
                    MoreJob(1, "孙权"),
                    MoreJob(1, "孙策"),
                    MoreJob(1, "孙尚香"),
                    MoreJob(1, "黄盖"),
                    MoreJob(1, "程普")
                )
            )

            mLoadMoreAdapter.loadMoreSuccess()

        }, 500)

    }

    private fun initData() {

        val newList = listOf(
            MoreJob(1, "刘备"),
            MoreJob(1, "张飞"),
            MoreJob(2, "曹操"),
            MoreJob(1, "赵云"),
            MoreJob(2, "曹仁"),
            MoreJob(1, "马超"),
            MoreJob(1, "黄忠"),
            MoreJob(1, "诸葛亮"),
            MoreJob(2, "夏侯惇"),
            MoreJob(1, "姜维"),
            MoreJob(1, "王平"),
            MoreJob(2, "徐晃"),
            MoreJob(1, "魏延"),
            MoreJob(2, "张辽"),
            MoreJob(2, "曹真"),
            MoreJob(1, "刘禅"),
            MoreJob(1, "张苞"),
            MoreJob(2, "司马懿"),
        )

        mAdapter.addDataList(newList)

    }

运行效果:

rv_01.gif

四、对RV.Adapter的装饰,组合头脚布局和LoadMore的功能

其实按道理来说我们两个装饰器是可以一起使用的。

如果想一起用,那么可以把添加头布局的装饰器当构造传入到加载更多的装饰器中。这样加载更多的装饰器就可以计算头布局和脚布局,才能得出正确的索引。装饰器是可以嵌套套娃使用的。

但是现在Adapter的情况有一点特殊,我们需要处理真正Item的索引,当我们添加了头布局,脚布局,又启用了LoadMore模块,那么真正Item的索引计算就有问题。

我们一种做法就是把添加头布局脚布局的装饰器当做构造传入到LoadMore的装饰器中,就可以拿到头布局脚布局的数量,从而计算索引。

另一种做法就是把头布局脚布局的装饰器和加载更多的装饰器合并为一个装饰器,这也是常用的一种方案。

具体的代码如下:(PS:为什么贴图不放源码?代码太多了不方便观看,其实代码是开源的,如果有需求可以去文末源码中自取。)

image.png image.png image.png image.png image.png image.png image.png image.png image.png

我们有时候需要处理减去头布局的索引,有时候需要判断去除脚布局的索引,特别是判断当前索引是属于哪一个类型的时候,我们还需要对当前索引做出判断。

使用起来大家应该都会,这里放一下运行的效果图:

rv_02.gif

总结

RV的一些封装是我们开发项目必不可少的,有些人使用的是框架,有些人使用的自己的封装,一些框架封装的很好,功能很多,但是要注意看我们是不是用得到这些功能,比如我们的项目要求不高,只需要一些添加头布局,脚布局等基本功能的RV,我们完全可以使用自己的封装,更加的灵活,方便修改。

如果当前场景只是一个普通的列表展示,那么我们就只需要普通的 BaseRVAdapter ,如果当然的场景是需要加载更多的RV列表或添加头部的功能,我们也可以使用上述的装饰器来包装 BaseRVAdapter ,从而灵活的实现我们的需求。

最后说明一下,本文的代码有一点多,因为都是重要代码,需要全部贴出来才能展示逻辑,虽然如此还是有一些不重要的代码没有贴出来,一些布局文件没有贴出来,如果大家有兴趣可以查看源码自取。本文全部代码都在里面。

好了,本期内容如讲的不到位或错漏的地方,希望同学们可以指出交流。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。