导读:本期聚焦于大象创作的《Android RecyclerView万能适配器如何封装才能避免重复代码?》,敬请观看详情。一个项目里如果有七八个列表页,每个页面都重写一套 RecyclerView.Adapter,维护起来会非常麻烦。万能适配器的思路是把 ViewHolder 创建、条目绑定、点击事件这些公共逻辑集中到基类中,只暴露一个抽象方法让子类填充数据。这样新增列表页只需关心布局 id 和控件赋值。文章会从基类封装开始,逐步加入多布局支持、Item 点击和长按事件、子控件事件传递,以及 DiffUtil 局部刷新。通过完整的 Java 代码示例,展示如何让适配器代码从上百行缩到十几行,同时保留扩展性。读完以后,你可以直接把这套万能适配器用到自己的项目里,也可以根据业务继续扩展。

RecyclerView 的适配器逻辑本身并不复杂,但一个项目里往往同时存在用户列表、订单列表、消息列表等多个页面,每个页面的 Adapter 都要重写一遍 onCreateViewHolder、onBindViewHolder,还要处理 ViewHolder 缓存和点击回调。这样下来,单个适配器动辄上百行,而且大量代码高度相似。万能适配器的目标就是把这部分重复工作收敛到基类,让具体业务适配器只保留布局声明和控件赋值两个动作。

Android RecyclerView万能适配器如何封装才能避免重复代码?

一、万能适配器到底省掉哪些重复代码

先拆解一下传统写法。一个完整的 RecyclerView.Adapter 基本包含五个固定部分:创建条目布局对应的 View、构造 ViewHolder、在 onBindViewHolder 中执行 findViewById 并赋值、维护数据集合、绑定条目点击事件。除了数据集合的类型和控件 id 不同,其余逻辑几乎可以原样复制。项目里的列表页越多,这套模板出现的次数就越多,维护时要改动任何一处都容易漏改。

万能适配器通过泛型基类统一管理数据集合和布局填充,同时用一个可以复用控件引用的 BaseViewHolder 替代分散的 findViewById。这样一来,新增一个列表页,只需要在子类构造函数里传入布局 id,再在 convert 回调中给控件塞值即可。像条目点击、长按、子控件点击这些公共行为,也可以提前在基类中预留接口,业务方按需实现。

从实际效果看,一个原本需要重写 4 到 5 个方法、代码量在一百行左右的适配器,使用万能适配器后可以缩短到十几行。更重要的是,当项目里出现几十个列表页时,基类只需要维护一处,扩展和排查问题都会简单很多。

二、BaseViewHolder 与 BaseQuickAdapter 的核心封装

BaseViewHolder 是万能适配器的第一层基础。RecyclerView 的条目 View 在滚动过程中会不断复用,如果每次绑定都重新 findViewById,虽然不会造成功能错误,但会带来不必要的性能开销。BaseViewHolder 内部使用 SparseArray 缓存已经找到的 View 引用,第一次查找后直接复用,后续绑定只做赋值操作。

public class BaseViewHolder extends RecyclerView.ViewHolder {
    private SparseArray<View> views;

    public BaseViewHolder(View itemView) {
        super(itemView);
        views = new SparseArray<>();
    }

    public <T extends View> T getView(int viewId) {
        View view = views.get(viewId);
        if (view == null) {
            view = itemView.findViewById(viewId);
            views.put(viewId, view);
        }
        return (T) view;
    }

    public BaseViewHolder setText(int viewId, CharSequence text) {
        TextView tv = getView(viewId);
        tv.setText(text);
        return this;
    }

    public BaseViewHolder setImageResource(int viewId, int resId) {
        ImageView iv = getView(viewId);
        iv.setImageResource(resId);
        return this;
    }
}

上面的 getView 方法使用泛型自动推断控件类型,调用方不需要强转。setText 和 setImageResource 返回 BaseViewHolder 自身,方便链式调用。实际项目中还可以继续扩展 setBackgroundColor、setVisibility 等常用方法,但不要一次性加太多,保持基类精简。

BaseQuickAdapter 则负责数据集合、布局填充和绑定流程。它继承自 RecyclerView.Adapter<BaseViewHolder>,并持有泛型数据集合 List<T>。子类只需要实现一个抽象方法 convert,在其中完成条目控件和数据的绑定。

public abstract class BaseQuickAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
    protected List<T> data;
    protected int layoutResId;

    public BaseQuickAdapter(int layoutResId, List<T> data) {
        this.layoutResId = layoutResId;
        this.data = data == null ? new ArrayList<>() : data;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
        return new BaseViewHolder(view);
    }

    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        convert(holder, data.get(position), position);
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    protected abstract void convert(BaseViewHolder holder, T item, int position);
}

有了这个基类,业务适配器的代码就变得非常干净。下面是一个用户列表的适配器,只需要声明 item 布局,再把用户名和年龄设置到对应控件上即可。

public class UserAdapter extends BaseQuickAdapter<User> {
    public UserAdapter(List<User> data) {
        super(R.layout.item_user, data);
    }

    @Override
    protected void convert(BaseViewHolder holder, User item, int position) {
        holder.setText(R.id.tv_name, item.getName())
              .setText(R.id.tv_age, String.valueOf(item.getAge()));
    }
}

这里不再出现 onCreateViewHolder 和 getItemCount,也不用关心控件查找。对于只需要单布局的列表,这套封装已经足够使用。需要注意的是,构造函数传入的 data 应该是同一个引用,后续增删数据时直接操作这个集合,再调用 notify 系列方法即可。

三、多布局支持如何设计

实际业务中列表很少只有一种条目样式。聊天消息有文本、图片、语音,信息流有标题卡片和视频卡片,订单列表有普通订单和促销订单。如果只为单一布局设计基类,遇到多布局时还是得回到原生 Adapter 的 getItemViewType 和 onCreateViewHolder 里做判断,万能适配器的优势就会打折扣。

多布局的本质是让 RecyclerView 根据条目类型创建不同 layoutId 的 View。基类可以先抽象出两个方法:getViewType 用于返回当前条目的类型,getLayoutId 根据类型返回布局资源 id。子类不再直接指定全局 layoutResId,而是把布局选择逻辑交给这两个方法。

public abstract class MultiTypeAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
    protected List<T> data;

    public MultiTypeAdapter(List<T> data) {
        this.data = data == null ? new ArrayList<>() : data;
    }

    protected abstract int getViewType(int position);

    protected abstract int getLayoutId(int viewType);

    protected abstract void convert(BaseViewHolder holder, T item, int position);

    @Override
    public int getItemViewType(int position) {
        return getViewType(position);
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        int layoutId = getLayoutId(viewType);
        View view = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
        return new BaseViewHolder(view);
    }

    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        convert(holder, data.get(position), position);
    }

    @Override
    public int getItemCount() {
        return data.size();
    }
}

子类实现时,只需要关心自己的数据类型如何映射到布局类型。比如消息列表包含文本和图片两种类型,可以根据消息对象的 type 字段返回 TYPE_TEXT 或 TYPE_IMAGE,再在 getLayoutId 中映射到不同的 layout 文件。

public class MessageAdapter extends MultiTypeAdapter<Message> {
    public static final int TYPE_TEXT = 1;
    public static final int TYPE_IMAGE = 2;

    public MessageAdapter(List<Message> data) {
        super(data);
    }

    @Override
    protected int getViewType(int position) {
        return data.get(position).getType();
    }

    @Override
    protected int getLayoutId(int viewType) {
        if (viewType == TYPE_IMAGE) {
            return R.layout.item_message_image;
        }
        return R.layout.item_message_text;
    }

    @Override
    protected void convert(BaseViewHolder holder, Message item, int position) {
        if (item.getType() == TYPE_IMAGE) {
            holder.setImageResource(R.id.iv_image, item.getImageRes());
        } else {
            holder.setText(R.id.tv_content, item.getContent());
        }
    }
}

这种设计把多布局判断集中在了适配器内部,Activity 或 Fragment 中仍然只调用 setAdapter,不需要感知布局类型。若后续再增加新的消息类型,只需要新增一个布局文件,并在 getViewType 和 getLayoutId 中加上对应分支,不会影响其他类型的展示。

四、点击事件与 DiffUtil 局部刷新

条目点击是列表交互里最高频的需求。初学者常在 Activity 里通过 RecyclerView 的 position 反查数据,但这种做法在数据增删后容易出现角标错位。更好的方式是把点击事件收口到适配器内部,直接回调被点击的数据对象,而不是只回传 position。

在基类中定义一个带泛型的点击接口,并暴露 set 方法。onBindViewHolder 里给 itemView 设置点击监听,通过 getAdapterPosition 获取当前有效位置,再把对应的数据对象传给接口。

public interface OnItemClickListener<T> {
    void onItemClick(View view, T item, int position);
}

private OnItemClickListener<T> itemClickListener;

public void setOnItemClickListener(OnItemClickListener<T> listener) {
    this.itemClickListener = listener;
}

@Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
    T item = data.get(position);
    holder.itemView.setOnClickListener(v -> {
        int pos = holder.getAdapterPosition();
        if (pos != RecyclerView.NO_POSITION && itemClickListener != null) {
            itemClickListener.onItemClick(v, data.get(pos), pos);
        }
    });
    convert(holder, item, position);
}

子控件点击也类似,可以在 convert 中通过 holder.getView(R.id.btn_delete).setOnClickListener(v -> ...) 处理。不过更通用的做法是在基类里增加一个 OnItemChildClickListener,保存控件 id 和回调,避免在业务适配器里写大段匿名内部类。

数据更新方面,直接调用 notifyDataSetChanged 会刷新整个列表,不仅浪费资源,还会导致条目闪烁和动画丢失。DiffUtil 可以根据新旧数据源计算差异,只更新发生变化的条目。把 DiffUtil 的计算过程封装到 setNewData 中,业务层更新数据时就会自动获得局部刷新能力。

public void setNewData(List<T> newData) {
    if (newData == null) {
        return;
    }
    DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
        @Override
        public int getOldListSize() {
            return data.size();
        }

        @Override
        public int getNewListSize() {
            return newData.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            T oldItem = data.get(oldItemPosition);
            T newItem = newData.get(newItemPosition);
            return oldItem.getId().equals(newItem.getId());
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            T oldItem = data.get(oldItemPosition);
            T newItem = newData.get(newItemPosition);
            return oldItem.equals(newItem);
        }
    });
    data.clear();
    data.addAll(newData);
    result.dispatchUpdatesTo(this);
}

这里假设数据对象有 getId 方法返回唯一标识,areItemsTheSame 只比较 id,areContentsTheSame 再比较内容。如果两个方法都返回相同结果,DiffUtil 会跳过该条目,从而只刷新真正改变的部分。使用万能适配器时,把这些能力预置在基类里,业务层就可以花更少时间处理列表维护,把精力放在数据来源和页面交互上。

RecyclerView万能适配器Android修改时间:2026-09-22 05:51:27

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0922/60362.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。