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

一、万能适配器到底省掉哪些重复代码
先拆解一下传统写法。一个完整的 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