RecyclerView 多类型Adapter的最佳实践

本文探讨如何在Android开发中优化RecyclerView的多类型Adapter,通过定义接口和ViewHolder,减少重复类型判断,提高代码可读性和可维护性。实践中介绍了接口定义、ViewHolder设计以及Adapter的实现,展示了文字和图片显示的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android开发中会有很多重复代码,更会有有很多臃肿代码。随着业务逻辑的复杂度增加,代码逻辑会显得混乱不堪,相信大家都有在茫茫码海中寻找那一小句代码的那种无力感,这种现象在Adapter中显得尤为突出。那么有没有一种方法能减少我们定位代码的时间,甚至在添加很多个类型之后,依然能够快速,准确地定位到关键代码的方法呢?肯定是有的,而且有很多种,今天我想分享的也是其中一种。

思考

Adapter的常规套路

在写Adapter时,一般都会经过这么几个步骤
1. 继承RecyclerView.Adapter类;
2. 继承RecyclerView.ViewHolder,创建视图缓存类,并在这个类中添加属性,用来缓存我们的控件;
3. 重写

public int getItemCount()

返回实际需要显示的视图项数
4. 重写

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)

这一步有两个重要的问题需要解决。第一,根据viewType创建对应的视图,第二用创建好的视图初始化第二步中创建的ViewHolder类的对象,并返回;
5. 可选。多视图的情况下还需要重写

public int getItemViewType(int position)

用来让Adapter在合适的位置显示正确的视图。
6. 重写

public void onBindViewHolder(ViewHolder holder, int position)

将数据真正地绑定到对应的视图上。
所以常规套路的大概代码形式是这种画风的

/**
 * Created by Andy on 2016/12/31.
 * QQ:632518410
 */

public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.NormalViewHolder> {
    private static final int TYPE_TEXT=0;
    private static final int TYPE_IMAGE=1;
    private Context context;

    public NormalAdapter(Context context) {
        this.context = context;
    }

    @Override
    public int getItemViewType(int position) {
        return 0==position%2?TYPE_TEXT:TYPE_IMAGE;
    }

    @Override
    public NormalViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (TYPE_TEXT == viewType) {
            return new NormalViewHolder(LayoutInflater.from(context).inflate(R.layout.item_text, parent, false), viewType);
        }else {
            return new NormalViewHolder(LayoutInflater.from(context).inflate(R.layout.item_image, parent, false), viewType);
        }
    }

    @Override
    public void onBindViewHolder(NormalViewHolder holder, int position) {
        if (0==position%2){
            holder.tvText.setText("Hello World");
        }else {
            holder.imImage.setImageResource(R.mipmap.ic_launcher);
        }
    }

    @Override
    public int getItemCount() {
        return 100;
    }

    class NormalViewHolder extends RecyclerView.ViewHolder {
        TextView tvText;
        ImageView imImage;
        public NormalViewHolder(View itemView,int type) {
            super(itemView);
            if (TYPE_TEXT == type) {
                tvText = (TextView) itemView.findViewById(R.id.item_text);
            }else {
                imImage = (ImageView) itemView.findViewById(R.id.item_image);
            }
        }
    }
}

大家看出问题了吗?我们在getItemViewType,onCreateViewHolder,onBindViewHolder竟然进行了三次类型判断,而且都是重复的类型判断,这种代码在多视图的环境中无疑是一种灾难。我们需要花费大把的时间来确认我们的类型是否正确,对象是否正确,绑定是否正确。尤其在数据复杂的情况下,这种代码的杀伤力简直就是致命的。

为什么

我们只想将数据绑定到视图上,却做了那么多无关痛痒的活,而这些活仅仅是为了保证数据绑定的准确性。整个过程中,我们看似只涉及到两个对象,数据和视图,实际上我们却需要从对象的海洋中花费大把时间找到那两个对应的对象。既然这种对应关系是实际存在的,那么我们为什么要手动地把它们分开再合并起来呢?同理,用面向对象的思维来思考这个问题,既然数据要绑定到视图上,那么它是不是自己就应该知道怎样绑定,是不是就应该知道它自己有多少数据需要展示,是不是就应该知道它要用怎样的视图形式来展示?

思路

根据Adapter的常规套路,我们可以得到一个数据的绑定过程。要不要在这个位置展示(getItemViewType),展示的视图是怎样的(onCreateViewHolder),怎样展示(onBindViewHolder),展示多少个(getItemCount),这都是固定的过程。那么一想到固定,一想到通用,大家想到了什么?对的,那就是interface。我们需要展示什么数据,就依据这些数据实现这个interface,那么是不是就不再需要对这些各种各样的数据进行一次又一次重复的判断了呢?

实践

既然,思路已经出来了,那么我们就开始用代码的形式表现出来吧。

interface 定义

根据思考阶段的思路,我们这个接口是完成数据的绑定过程。而这个过程会经历四个阶段:
- 判断是否需要展示
- 获取展示视图
- 怎样展示
- 展示多少内容
那么,我们将这四个过程定义成interface之后,长成这样:

/**
 * Created by Andy on 2016/12/31.
 * QQ:632518410
 *
 * 数据绑定流程
 */

public interface IDataHolder {
    /*判断该位置是否需要处理*/
    boolean isHandleable(int position);

    /*获取数据视图*/
    int layout();

    /*绑定视图*/
    void bind(BetterViewHolder holder,int position);

    /*显示数据的数目*/
    int sizeToShow();
}

流程是出来了,但是怎样才能把interface应用到Adapter中呢?很明显,我们的interface是对应Adapter的四个回调方法的。但是问题就是,现在不止一种数据,怎么办呢?难道我们还得走上重复判断的老路?当然不是,我们还忽略了一个重要的类ViewHolder。我们知道,通常情况下,一种数据类型是对应着一种视图,那么我们是不是能够的直接将这种对应关系建立直接联系。建立联系的方法有很多,这里我们希望这种联系持续存在在对象的整个生命周期中,所以,我们选择了组合的方式,让ViewHolder持有IDataHolder对象的引用。

定义ViewHolder

在常规的Adapter中ViewHolder类负责对视图进行缓存,所以一般都会持有控件的引用,方便在绑定视图的时候重用。但是在我们现在的这种通用实现中,我们并不知道实际使用过程中会用到哪些控件,所以不能像常规套路那样,将控件都用属性缓存起来。那有没有什么方法能根据资源ID就能快速定位相应的控件实例呢?很明显,这是一种映射关系,首当其冲当然选择Map了。但是有没有更优一点的选择呢?答案是SparseArray。它是SDK提供的更高效的键值映射类。
当然,ViewHolder不仅能完成缓存工作,也可以定义一些通用方法,减少代码量。如可根据资源ID快速设置控件的属性值。当然,最重要的是,让ViewHolder缓存它对应的IDataHolder对象,从而避免重复判断类型。
由功能定义,可快速得到实现如下:
1. 一个SparseArray属性,缓存视图组件;
2. 一个IDataHolder属性,关联对应数据;
3. 几个通用方法,根据资源ID获取视图,根据资源ID设置视图属性等。
所以,简单的ViewHolder类可以定义如下:

/**
 * Created by Andy on 2016/12/31.
 * QQ:632518410
 *
 * 视图缓存
 */

public class BetterViewHolder extends RecyclerView.ViewHolder {
    private IDataHolder mDataHodler;
    private SparseArray<View> mCache;

    public BetterViewHolder(View itemView, IDataHolder mDataHodler) {
        super(itemView);
        this.mDataHodler = mDataHodler;
        this.mCache = new SparseArray<>();
    }

    /*获取数据*/
    public IDataHolder getDataHodler() {
        return mDataHodler;
    }

    /*获取视图*/
    public <T> T getView(int resId) {
        View view = mCache.get(resId);
        if (null == view) {
            view = itemView.findViewById(resId);
            mCache.put(resId, view);
        }
        return (T) view;
    }

    /*设置TextView文字*/
    public void setText(int resId, CharSequence text) {
        TextView textView = getView(resId);
        textView.setText(text);
    }

    /*设置ImageView的图片*/
    public void setImage(int resId, int imageRes) {
        ImageView imageView = getView(resId);
        imageView.setImageResource(imageRes);
    }
}

将interface应用到Adapter中去

回顾Adapter的回调,数据的绑定经过获取类型,创建视图,绑定数据,限制条目等阶段。现在我们逐一实现这些过程。

获取类型

在获取类型阶段,对应的接口方法是isHandleable(),用于判断position这个位置自己是否需要处理?但是多类型时,怎么判断呢?这里我用了循环的方法,逐一检测各个IDataHodler对象,检测到某个返回TRUE后,返回对应的布局ID,具体实现如下:

 @Override
    public int getItemViewType(int position) {
        for (int i = 0; i < mDataHolders.size(); i++) {
            IDataHolder holder = mDataHolders.valueAt(i);
            if (holder.isHandleable(position)) {
                return holder.layout();
            }
        }
        return 0;
    }
创建视图

在判断类型时,我们返回了对应数据的视图ID,所以我们直接使用viewType来创建视图,并用创建好的视图,viewType对应的的IDataHolder对象初始化ViewHolder对象,所以在这里我们不需要再判断视图和数据的对应关系了,已经是正确的了。参考实现如下:

@Override
    public BetterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new BetterViewHolder(LayoutInflater.from(context).inflate(viewType,parent,false),mDataHolders.get(viewType));
    }
绑定视图

绑定视图这一步骤现在变成最简单的了,因为,我们不再在onBindViewHolder方法中执行真正的绑定操作,而是交给实际的数据对象去处理,这个方法仅仅起到触发绑定的作用。同样,这里也不再需要判断IDataHolder的实际类型了,因为我们的ViewHolder已经缓存着对应的IDataHolder对象的引用,我们只需要调用该对象对应的bind方法即可。参考实现如下:

@Override
    public void onBindViewHolder(BetterViewHolder holder, int position) {
        holder.getDataHodler().bind(holder,position);
    }
获取显示条目

因为,每个数据类型都实现了interface的sizeToShow方法,所以我们只需要计算所有的数据项的条目,返回就行了。很简单,就不详细说了,参考实现如下:

@Override
    public int getItemCount() {
        int all=0;
        for (int i = 0; i < mDataHolders.size(); i++) {
            all+=mDataHolders.valueAt(i).sizeToShow();
        }
        return all;
    }

至此,所有的设计已经实现了。下面进入到实战阶段。

实战

目标:实现思考阶段的功能
分析:我们需要展示两种不同的数据,那么,我们需要定义两个不同的数据类型,分别实现IDataHolder这个interface。

显示文字

参考实现如下:

/**
 * Created by Andy on 2016/12/31.
 * QQ:632518410
 *
 * 显示数据
 */

public class TextDataHolder implements IDataHolder {
    @Override
    public boolean isHandleable(int position) {
        return 0==position%2;
    }

    @Override
    public int layout() {
        return R.layout.item_text;
    }

    @Override
    public void bind(BetterViewHolder holder, int position) {
        holder.setText(R.id.item_text,"Hello World!");
    }

    @Override
    public int sizeToShow() {
        return 50;
    }
}

显示图片

参考实现如下:

/**
 * Created by Andy on 2016/12/31.
 * QQ:632518410
 *
 * 显示图片
 */

public class ImageDataHolder implements IDataHolder {
    @Override
    public boolean isHandleable(int position) {
        return 0!=position%2;
    }

    @Override
    public int layout() {
        return R.layout.item_image;
    }

    @Override
    public void bind(BetterViewHolder holder, int position) {
        holder.setImage(R.id.item_image,R.mipmap.ic_launcher);
    }

    @Override
    public int sizeToShow() {
        return 50;
    }
}

怎么样,是不是很清爽?不再有重复的判断,不再有眼花缭乱的嵌套。虽然多了几个类,但是获得了更好的可读性和可维护性。

总结

实现这种Adapter的步骤总共需要定义三个类,第一个是数据类,负责处理具体的绑定过程;第二个是视图类,负责缓存视图组件,创建通用方法和建立数据对象的映射,第三个是Adapter类,负责将这些数据,视图组合起来,并绑定相应的回调。这样做的好处是保持多数据类型的环境中,代码的清晰和可维护性。缺点是多创建了一些类。
最后参考源码在GitHub上。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值