☰
RecyclerView与ListView适配器封装实践:从ViewHolder到加载更多全攻略
2026/10/18 16:28:23 网站建设 项目流程

真要说起来,RecyclerView和ListView的封装其实是个老生常谈的话题,但只要你的项目还在用原生列表控件,这个问题就永远绕不过去。尤其是当你手头同时管着两三个项目,每个项目里都有列表页,每个列表页的适配器代码都长得差不多,又各自为政的时候,你就会意识到封装不是锦上添花,而是纯纯的刚需。

我最早被这个问题折磨是在接手一个历史项目的时候。项目里ListView和RecyclerView混用,适配器从BaseAdapter到RecyclerView.Adapter都有,里面还直接塞了数据加载逻辑、点击事件,甚至有把网络回调写进ViewHolder里的。当时我就觉得,这玩意要是再不梳理一下,后面接手的兄弟大概率会在某个深夜问候前同事全家。

所以这篇文章不打算讲那种大而全的框架设计,也不搞什么高深到看不懂的抽象层次,就讲我在实际操作中是怎么一步步封装ListView和RecyclerView的,以及封装过程中踩过的那些坑。目标很明确:让你看完之后能直接动手,给自己项目里的列表控件做一个干净利落的封装。

1. 列表控件封装的整体思路拆解

封装列表控件,第一步不是写代码,而是想清楚你到底要解决什么问题。抛开业务功能差异不谈,ListView和RecyclerView在使用上有一个共同的痛点:适配器里全是样板代码。

比如一个最简单的人员列表,最原始的写法就是这样:

public class PersonAdapter extends BaseAdapter { private Context context; private List<Person> persons; private LayoutInflater inflater; public PersonAdapter(Context context, List<Person> persons) { this.context = context; this.persons = persons; this.inflater = LayoutInflater.from(context); } @Override public int getCount() { return persons == null ? 0 : persons.size(); } @Override public Object getItem(int position) { return persons.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.item_person, parent, false); holder = new ViewHolder(); holder.nameTv = convertView.findViewById(R.id.tv_name); holder.ageTv = convertView.findViewById(R.id.tv_age); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } Person person = persons.get(position); holder.nameTv.setText(person.getName()); holder.ageTv.setText(String.valueOf(person.getAge())); return convertView; } static class ViewHolder { TextView nameTv; TextView ageTv; } }

这段代码放在一个Demo里没什么问题,但你有十个八个这样的列表页,你就会发现getView里面反复在做同一件事情:判空convertView、加载布局、findViewById、setTag、绑定数据。 RecyclerView虽然写法变成了onCreateViewHolder和onBindViewHolder,但本质上还是这一套流程。区别只是RecyclerView强制了你用ViewHolder,不至于让你像ListView那样在性能上翻车。

封装的思路其实就是把这一套固定的流程抽出来,把变化的部分留成接口或者泛型参数。我需要做的就是把“加载布局”和“绑定数据”这两件事抽象出来,其他东西全部收敛到基类里。

基于这个想法,我当时的封装拆成了三层结构:

  • 基类适配器层:负责ViewHolder的创建复用、数据集合的管理、Item点击事件的回调。
  • 通用ViewHolder层:把findViewById和控件缓存封装掉,统一管理itemView。
  • 回调接口层:让外部只需要关心每条Item的数据和UI绑定关系。

这个三层结构的好处是,职责边界非常清楚。底层做缓存复用,中层做视图管理,上层做业务绑定。后面不管你是列表还是网格,是单类型还是多类型,都是在这个骨架上去扩展,不需要动底层逻辑。

2. 通用ViewHolder与基类Adapter的封装实现

2.1 通用ViewHolder的设计

ViewHolder的核心作用是什么?缓存itemView里的子控件,避免每次绑定数据都去findViewById一遍。那封装的第一步就是把这一层做到极致,让业务代码不再感知ViewHolder具体的创建和缓存过程。

这里我首选的设计是用一个Map来按ID缓存控件:

public class CommonViewHolder { private final SparseArray<View> views = new SparseArray<>(); private final View convertView; private CommonViewHolder(View convertView) { this.convertView = convertView; } public static CommonViewHolder create(View convertView, ViewGroup parent, int layoutId) { if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()) .inflate(layoutId, parent, false); CommonViewHolder holder = new CommonViewHolder(convertView); convertView.setTag(holder); return holder; } return (CommonViewHolder) convertView.getTag(); } @SuppressWarnings("unchecked") public <T extends View> T getView(int viewId) { View view = views.get(viewId); if (view == null) { view = convertView.findViewById(viewId); views.put(viewId, view); } return (T) view; } public View getConvertView() { return convertView; } }

这里用了SparseArray而不是HashMap,原因很简单:SparseArray是Android官方推荐的优化方案,它避免了整数键的装箱拆箱操作,在数据量少的时候,性能反而比HashMap更好。这种细节在列表滚动的频繁调用场景下是有意义的。

getView方法里SparseArray也是做缓存,第一次findViewById之后就把控件放进去,下次直接从缓存取。这样做的性能提升可能比你在bindData里少写两行if判断还明显,而且代码写起来更舒服。

有了CommonViewHolder,ListView的适配器就可以这样写:

public abstract class CommonAdapter<T> extends BaseAdapter { private final LayoutInflater inflater; private List<T> dataList; public CommonAdapter(Context context, List<T> dataList) { this.inflater = LayoutInflater.from(context); this.dataList = dataList == null ? new ArrayList<>() : dataList; } @Override public int getCount() { return dataList.size(); } @Override public T getItem(int position) { return dataList.get(position); } @Override public long getItemId(int position) { return position; } public abstract int getLayoutId(int position, T item); public abstract void bindData(CommonViewHolder holder, T item, int position); @Override public View getView(int position, View convertView, ViewGroup parent) { T item = getItem(position); CommonViewHolder holder = CommonViewHolder.create(convertView, parent, getLayoutId(position, item)); bindData(holder, item, position); return holder.getConvertView(); } }

getLayoutId被设计成带参数是有讲究的。因为实际业务里,一个ListView同一屏可能出现多种条目样式,比如列表里混着标签类型的item和内容类型的item。如果getLayoutId是固定返回一个布局,那多类型就没办法实现了。这里把position和item都传进去,子类可以根据数据自行判断该用哪个布局。

2.2 RecyclerView版的基类Adapter

RecyclerView的封装思路和ListView类似,但有几个地方不同。首先是它强制你用ViewHolder,所以CommonViewHolder需要直接继承RecyclerView.ViewHolder,这个类的构造参数就是ItemView,恰好可以让我们直接把convertView传进去。

public class RecyclerViewHolder extends RecyclerView.ViewHolder { private final SparseArray<View> views = new SparseArray<>(); public RecyclerViewHolder(View itemView) { super(itemView); } @SuppressWarnings("unchecked") 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; } }

对应的基类Adapter:

public abstract class CommonRvAdapter<T> extends RecyclerView.Adapter<RecyclerViewHolder> { protected List<T> dataList; public CommonRvAdapter(List<T> dataList) { this.dataList = dataList == null ? new ArrayList<>() : dataList; } @NonNull @Override public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { int layoutId = getLayoutId(viewType); View itemView = LayoutInflater.from(parent.getContext()) .inflate(layoutId, parent, false); return new RecyclerViewHolder(itemView); } @Override public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) { T item = dataList.get(position); bindData(holder, item, position); } @Override public int getItemViewType(int position) { return getViewType(position, dataList.get(position)); } @Override public int getItemCount() { return dataList.size(); } protected abstract int getLayoutId(int viewType); protected abstract void bindData(RecyclerViewHolder holder, T item, int position); protected int getViewType(int position, T item) { return 0; } }

这里getItemViewType默认返回0,子类如果有多类型需求就重写getViewType。getLayoutId只在onCreateViewHolder里用viewType去拿,所以单类型情况下子类只需要关心getLayoutId和bindData两个方法。

使用起来比ListView那个版本要清爽很多:

public class PersonListAdapter extends CommonRvAdapter<Person> { public PersonListAdapter(List<Person> dataList) { super(dataList); } @Override protected int getLayoutId(int viewType) { return R.layout.item_person; } @Override protected void bindData(RecyclerViewHolder holder, Person person, int position) { holder.getView(R.id.tv_name).setText(person.getName()); holder.getView(R.id.tv_age).setText(String.valueOf(person.getAge())); } }

说句实在话,写到这一步之后,我基本上就告别了在子类里再去覆盖onBindViewHolder这种操作了。后面新增一个列表页,只需要新建一个Adapter继承CommonRvAdapter,实现getLayoutId和bindData,三分钟搞定。代码量至少减少一半以上。

3. 封装点击事件与多类型列表的支持

3.1 点击事件的统一回调

列表Item的点击事件是使用频率最高的交互之一。如果不封装,那么每个Adapter里都要创建一个OnClickListener,然后在bindData或者getView里给每个需要点击的控件setOnClickListener,再把position通过holder.getAdapterPosition()拿回来。这个流程不难,但重复率极高。

我的做法是在基类里加一个setOnItemClickListener接口,然后在onBindViewHolder里统一处理:

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

基类中增加字段和设置方法:

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

然后在onBindViewHolder里给itemView设置点击监听:

@Override public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) { T item = dataList.get(position); holder.itemView.setOnClickListener(v -> { if (itemClickListener != null) { itemClickListener.onItemClick(v, item, holder.getAdapterPosition()); } }); bindData(holder, item, position); }

这里有个细节:不要用position去绑定点击事件,因为在RecyclerView的Item复用机制下,position可能因为增删Item而变得不准确。在回调触发时才通过holder.getAdapterPosition()获取当前真实位置,才是安全的。ListView那边则可以直接用position,因为数据源和列表是同步的。

如果你的列表里Item内部还有子控件的点击事件,比如“收藏”按钮,LongClick等,那也可以在基类再提供一些辅助接口,比如OnItemChildClickListener和OnItemLongClickListener。但我的经验是,这类接口不要无脑往上堆,按需添加就行。一个基类如果接口太多,往后用起来反而累赘。

3.2 多类型列表的封装策略

前面提过基类的getViewType默认返回0,当同一个列表需要展示多种类型数据时,有两种做法。

第一种是重写getViewType和getLayoutId。这是最标准、最直观的方式:

private static final int TYPE_TEXT = 0; private static final int TYPE_IMAGE = 1; @Override protected int getViewType(int position, FeedInfo item) { if (item.getType() == FeedInfo.TYPE_TEXT) { return TYPE_TEXT; } return TYPE_IMAGE; } @Override protected int getLayoutId(int viewType) { if (viewType == TYPE_IMAGE) { return R.layout.item_feed_image; } return R.layout.item_feed_text; }

第二种是我后来在项目里用的更灵活的方式,就是把“判断类型”这件事交给数据本身去处理。具体怎么做呢?给数据源定义一个接口,让每个数据对象自己告诉适配器它对应的是哪种类型:

public interface MultiTypeItem { int getDataType(); }

公共Adapter里增加一个重载构造方法:

public abstract class CommonMultiTypeAdapter<T extends MultiTypeItem> extends RecyclerView.Adapter<RecyclerViewHolder> { private List<T> dataList; public CommonMultiTypeAdapter(List<T> dataList) { this.dataList = dataList; } @Override public int getItemViewType(int position) { return dataList.get(position).getDataType(); } @NonNull @Override public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { int layoutId = getLayoutId(viewType); View itemView = LayoutInflater.from(parent.getContext()) .inflate(layoutId, parent, false); return new RecyclerViewHolder(itemView); } }

然后在外部维护一个layoutId和viewType的映射关系,就能实现完全基于数据的多类型渲染。这种思路在面对那种不断变化类型的列表时尤其好用,因为后续新增类型,你只需要增加一个数据实现类,不用去改Adapter。

不过说实话,如果只是两三种固定类型,用第一种方法就够了。第二种适合做动态运营位、动态卡片这类需求,过度设计也是一种负担。

4. 列表的加载更多、空布局与头部布局那些事

4.1 RecyclerView的加载更多封装

真到了业务层面,最影响开发效率的不是基础绑定,而是加载更多、空布局、Header/Footer这些在列表开发中绕不开的实际问题。

加载更多的常规做法是给Adapter加一个“底部状态Item”。这个Item只有三种状态:加载中、上拉加载、没有更多了。这三态分别对应三种UI展示:转圈动画、文字提示、尾巴样式。

我在基类里的实现是这样的:

public static final int STATE_DEFAULT = 0; public static final int STATE_LOADING = 1; public static final int STATE_FINISH = 2; public static final int STATE_FAIL = 3; private int loadState = STATE_DEFAULT; private boolean hasFooter = false;

增加设置状态的方法:

public void setLoadState(int state) { this.loadState = state; this.hasFooter = (state != STATE_DEFAULT); notifyDataSetChanged(); } public void setLoadFailed() { this.loadState = STATE_FAIL; notifyDataSetChanged(); }

然后在基类的getItemCount里把Footer这个位置算进去:

@Override public int getItemCount() { return dataList.size() + (hasFooter ? 1 : 0); }

onCreateViewHolder里用viewType判断是否有Footer类型的布局:

private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1; @Override public int getItemViewType(int position) { if (hasFooter && position == dataList.size()) { return TYPE_FOOTER; } return super.getItemViewType(position); }

加载更多的触发方式有两种选择。一种是给RecyclerView添加OnScrollListener,在滚动到倒数第几个Item的时候回调外部接口去拉下一页数据。另一种是用现成的工具类。这里我推荐第一种,自己手写,不依赖第三方库,逻辑也特别简单:

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); int totalItemCount = layoutManager.getItemCount(); if (lastVisibleItemPosition >= totalItemCount - 2 && dy > 0) { if (listener != null) { listener.onLoadMore(); } } } });

threshold设成2,意思是提前两个Item就触发加载,这样可以有效规避快速滑到底部时,网络请求还没返回,用户已经看到Footer“加载中”的尴尬。

4.2 空布局与Header的封装

空布局的处理方案,很多人会用“在Activity里根据数据源长度显示/隐藏一个空态View”的做法。这并不能说错,但从复用角度讲,每个页面都要写一遍这个逻辑很烦。更优雅的做法还是把空View放进RecyclerView的Adapter里,当数据为空时,Adapter返回一个空态布局的Item。

实现起来也简单:数据源长度为0并且开启了空布局开关时,getItemCount返回1,这个1对应的是一个空布局。这种做法的好处是,状态切换不需要操作多个控件,只需调用adapter.setEmptyViewVisible(true)即可,整个界面布局保持一致。

Header的封装思路也类似,本质上都是为Adapter维护一个额外的viewType队列。不过Header相对特殊,因为业务页面里Header的形态差异很大,有轮播图、搜索栏、分类入口,甚至还有嵌套的RecyclerView。我的建议是不要过度抽象Header,更不要试图搞成“万能Header”,否则你会在如何给Header传数据、如何刷新Header里的子控件这些破事上折腾很久。一句话总结:做到“支持一个Header布局+一个Footer加载状态”就够了,需要复杂Header的业务页面可以直接通过LayoutManager.addView等方式在RecyclerView外部组合。

4.3 ListView的Footer加载与EmptyView

ListView这边因为历史原因,本身自带set_empty_view和addFooterView,这算是一个原生优势。但ListView的加载更多封装起来反而比RecyclerView更麻烦一点,因为ListView的Footer一旦add进去就难移除,而且onItemClick的position会算上Footer的位置,不做转换就会点击错位。

我建议ListView的加载更多不要用ListView原生Footer机制,而是在getView里用getItemViewType判断最后一行的类型,手动渲染一个加载尾布局。原理跟RecyclerView的Footer思路一致,只是写法上结合BaseAdapter的getViewTypeCount和getItemViewType实现。这样能避免原生Footer引发的各种position坑。

5. 封装之外:性能和坑位盘点

5.1 性能对比:ListView与RecyclerView该怎么选

既然文章标题是“RecyclerView/ListView封装及使用”,那这两者的选型问题就必须说清楚。虽然说现在是RecyclerView的天下,但ListView并没有完全过气,尤其是一些轻量列表场景,特别是数据量很小、页面不复杂的时候,ListView依然可用,甚至代码量更少。

从封装角度看,RecyclerView的扩展性完胜ListView。RecyclerView的LayoutManager机制天然支持LinearLayout、GridLayout和StaggeredGridLayout三种布局,你封装一次Adapter,多场景复用起来毫无压力。ListView要做横向列表,就得靠HorizontalScrollView+LinearLayout硬凑,不仅卡,代码还难看。

再从性能角度说,RecyclerView全面胜出。它自带的ViewHolder复用机制是强制的,而ListView的ViewHolder效率取决于开发者的自觉。加上RecyclerView提供的setHasFixedSize优化、DiffUtil自动差量更新、预加载机制等,数据量大时优势很明显。我实测过一个1000条数据的列表,RecyclerView配合DiffUtil的增量刷新性能,比起ListView的notifyDataSetChanged全量刷新,流畅度差距肉眼可见。

所以结论很明确:如果是新项目,无脑选RecyclerView。如果是维护老项目,ListView代码又改不动,那就老老实实用ListView的原生方案优化性能,别强行混合穿插。

5.2 封装时容易踩的坑

封装实践这么久,有几个坑每次碰到都会让人头大,这里挑典型的列一列。

第一个坑是notifyDataSetChanged的乱用。很多初学封装的人,不管什么状态变化都直接唤notifyDataSetChanged,这会引发RecyclerView全局刷新,所有可见Item都会重新绑定一遍数据,代价很高。比如只改了某个Item里的一个TextView,其实完全可以定位到那个ViewHolder去更新。DiffUtil就是为这个问题设计的,但很多人封装时忽略了它。建议基类里留一个notifyDataChanged(List newData)的入口,内部自己跑DiffUtil:

public void refreshData(List<T> newData) { DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(dataList, newData)); dataList.clear(); dataList.addAll(newData); diffResult.dispatchUpdatesTo(this); }

DiffCallback里需要实现areItemsTheSame和areContentsTheSame两个方法,前者判断是不是同一条数据,后者判断内容有没有更新。这两个方法写得好不好直接影响刷新动画的准确性和性能。

第二个坑是RecyclerView的ItemAnimator在刷新数据时导致Item闪烁。尤其在多选、选中态切换的场景,如果数据更新后调notifyItemChanged,默认动画会有一个alpha闪动效果。解决的方案很简单,封装时提供一个开关,允许关闭动画:

recyclerView.setItemAnimator(null);

或者用自定义的DefaultItemAnimator并设置change动画的时长。

第三个坑是ListView的convertView复用导致数据错乱。典型现象就是滚动列表时,Item的图片或者文字会“跳”。根因是getView里对某个View的可见性做了控制,比如设置了visibility为GONE,但没有在else分支里恢复。所以封装ListView的bindData时,所有条件判断的状态一定要写全,尤其要保证默认状态是全的。

第四个坑是RecyclerView嵌套RecyclerView的场景。最常见的是纵向RecyclerView里有一个横向滑动的子列表。如果不做处理,子列表滑动时会和父列表争抢滑动事件,滑动体验就很糟糕。常规做法是在外层RecyclerView的onInterceptTouchEvent里判断,或者用一个自定义LayoutManager限制子列表只能横向滑动。封装层面能做的是,如果子列表数据量不大,直接禁用子列表的滑动,用嵌套滚动配合父列表滚动:

LinearLayoutManager childLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false); childRecyclerView.setLayoutManager(childLayoutManager); childRecyclerView.setNestedScrollingEnabled(false);

setNestedScrollingEnabled(false)这个方法,在嵌套滚动场景下能省掉大量滑动冲突的破事。不过到了Android 12以后,嵌套滚动的新机制又带来一些变化,这点需要大家在各自项目里实测一下。

第五个坑是BaseAdapter的getItemViewType返回值混淆。ListView里如果你不重写getItemViewType和getViewTypeCount,那列表里只有一个viewType,没问题。但只要出现了不同类型,一定要保证viewType从0开始连续递增。RecyclerView则相反,viewType越界会直接崩溃。封装时要警惕Integer开头的Type值,比如用Integer.MAX_VALUE给Footer当viewType,很容易在极端数据下冲突。

5.3 封装的心得与建议

做了这么久封装,我的体会是:封装不是越复杂越好,而是要卡在“够用”和“好用”之间。

什么叫够用?就是覆盖80%的列表场景,包括常规渲染、加载更多、点击事件、空态展示、多类型。什么叫好用?就是团队里的新人也能在半小时内学会,而不是打开基类源码还要研究半天,最后高血压发作。

在设计基类时,我给你的建议是遵循两条原则。

第一条原则是“泛型只处理数据源”。一个Adapter只负责一种类型的数据,数据的类型通过泛型参数限定掉。不要在Adapter内部做类型判断去处理多种数据源,否则你很快会被一堆if-else拖垮。

第二条原则是“Callback要小而精”。点击回调、加载更多回调、子控件回调,分开定义。不要搞一个万能Callback接口,里面十几个方法,子类都得空实现一堆,这种设计会让基类变得非常难用。

另外还有一个很实用的经验:不要把业务数据模型直接当Adapter的泛型。比如你从后端拿到了一个Person对象,但这个列表页要展示的信息可能包括“是否选中”、“是否加载完成”这种UI状态。把这些状态塞进Person里会让模型变得很脏。正确做法是定义一个新的ListItem类型,把Person数据包一层。

public class PersonListItem { public Person person; public boolean isSelected; }

这样Adapter的泛型就是PersonListItem,数据绑定和状态维护都干净得多。这个技巧看上去不起眼,但在列表交互频繁的项目里,用着真的会上瘾。

6. 常见问题与排查技巧实录

最后整理几个我在项目里实际遇到过的、排查起来比较耗时间的问题,做成速查格式,希望对你有帮助。

第一个常见问题是“滚动列表时Item内容闪烁”。排查方向:一是看Adapter的bindData里是否有对Item做全局的setVisibility或setAlpha操作,二是看RecyclerView的ItemAnimator是否设置了change动画。可以先关掉ItemAnimator,如果问题消失,就是动画的锅。

第二个常见问题是“Listview点击错位,点击第一行弹的是另一行的内容”。排查方向:检查有没有使用ListView原生的addHeaderView或者addFooterView。如果用了,点击事件的position必须减去Header的数量,Adapter里的getItemPosition也要做转换。

第三个常见问题是“RecyclerView滑动变卡”。排查方向不要一上来就优化布局层级,先看是不是在没有必要的地方使用了wrap_content作为高度,或者item中是否大量使用了layout_weight。再检查onBindViewHolder中是不是做了耗时操作,比如网络取图、计算属性。所有耗时操作都要异步化,位置信息通过holder.getBindingAdapterPosition()获取。

第四个常见问题是“EmptyView显示不出来”。这个最烦,因为通常不是代码写错了,而是数据结构的问题。检查Adapter里getItemCount是不是正确地考虑了emptyView的占位。如果使用了DiffUtil刷新数据,也要看DiffUtil回调之后有没有正确设置emptyView的可见状态。

第五个常见问题是“加载更多被重复触发”。我碰到过的一个经典场景是:在onScrolled里触发加载更多后没有做时间锁/状态锁,快速滑动时onScrolled会连续触发好几次。解决思路很简单,用加载状态做个保护:

private boolean isLoadingMore = false; private void triggerLoadMore() { if (isLoadingMore) { return; } isLoadingMore = true; // 发起网络请求,请求成功后置为false }

在封装层面,这需要基类提供一个setLoadingMore和isLoadingMore的开关,让外部加载完业务数据后再调用一次setLoadState(STATE_DEFAULT)解锁。

第六个常见问题是“多类型列表的itemViewType混乱,导致崩溃”。这种崩溃通常是getItemViewType的实现里,对不同数据的判断条件有重叠,或者是viewType用了一个不稳定的数值,比如用position当viewType。解决方法是把viewType定义成常量,用switch或者Map做映射,保证每个子类实现里返回的都是稳定且唯一的类型值。

写在你动手之前

个人认为,做RecyclerView和ListView的封装,不需要一上来就照着全网最全的开源框架抄,比如那个几十个类、七大特性的万能适配器库,那种方案维护成本极高,大部分项目扛不住。你要做的是先明确自己的业务规模,再根据实际情况抽取公共逻辑。

我在实际项目中用的这套封装,最终稳定下来的核心类也就三个左右:一个CommonViewHolder、一个CommonRvAdapter、一个对应的ListView版CommonAdapter。代码加起来大概几百行,却足以覆盖项目中90%以上的列表需求。如果某个页面出现了这三个类搞不定的场景,那这个页面大概率也不适合用通用Adapter,直接手写一个专用Adapter反而更快。

遇到这种情况,你完全不必纠结,封装的边界就在这里:公共代码抽出来复用,特殊页面保持独立。以我的经验,这种“七分通用、三分特化”的做法,是列表封装最舒服的甜点区。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询