基于DataBinding的RecyclerView Adapter基类封装与列表优化实践

📅 发布时间:2026/10/17 1:11:53
基于DataBinding的RecyclerView Adapter基类封装与列表优化实践
做Android开发久了RecyclerView的Adapter几乎成了每天都要写的东西。尤其是项目里已经全面切到DataBinding之后我发现不少团队的Adapter代码还停留在ViewHolder findViewById setText的老节奏里跟DataBinding完全是两套思路。今天把我在实际项目里沉淀出来的这套基于DataBinding的Adapter基类完整整理出来它把RecyclerView和DataBinding之间重复的那些胶水代码统一收敛业务方只需要关心item布局和一行绑定逻辑。如果你也在做列表页开发每天被重复的ViewHolder和notify代码折磨这篇文章可以直接参考。这个基类解决的核心问题很明确消灭重复。传统Adapter里有大量结构固定、只有局部变化的东西比如onCreateViewHolder里的inflate、onBindViewHolder里的类型转换、ViewHolder类的定义、itemCount的返回。这些代码在所有Adapter里几乎一模一样区别仅在于layout不同、绑定逻辑不同和数据源不同。把不变的逻辑收敛到基类把可变的用抽象方法暴露出去子类从几十行缩到几行这就是这套方案的价值。文章内容不止有代码还会从头讲清楚设计思路、两种常见扩展场景多类型列表、DiffUtil高效刷新以及在真实测试和上线过程中踩过的坑。适合正在用DataBinding、或者想引入DataBinding但担心Adapter改造麻烦的Android开发者。1. 为什么我写了一个Adapter基类来收拢重复代码1.1 传统Adapter写法每个列表都在复制粘贴同一套模板先看一个最普通的列表Adapter长什么样。假设是一个消息列表数据类是MessageItemitem布局是item_message.xml。很多人项目里现在还是这么写的class MessageAdapter( private val items: MutableListMessageItem ) : RecyclerView.AdapterMessageAdapter.MessageViewHolder() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_message, parent, false) return MessageViewHolder(view) } override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { val item items[position] holder.titleView.text item.title holder.contentView.text item.content holder.timeView.text formatTime(item.time) } override fun getItemCount(): Int items.size class MessageViewHolder(view: View) : RecyclerView.ViewHolder(view) { val titleView: TextView view.findViewById(R.id.tv_title) val contentView: TextView view.findViewById(R.id.tv_content) val timeView: TextView view.findViewById(R.id.tv_time) } }单看这一个Adapter觉得还行但如果项目里有几十个列表页每个模块都要写一套这样的ViewHolder、onCreateViewHolder、onBindViewHolder问题就放大了。不同列表之间真正不同的地方其实只有三处item布局id、数据类、绑定时给哪些View赋值。剩下的全是重复。改一个通用交互逻辑比如增加item点击统计、增加加载更多脚布局就得跑去几十个Adapter里改漏一个就是线上bug。这才是封装基类最核心的动机不是炫技而是把几十份重复代码压缩成一个通用模板。等列表页多了以后收益极其明显。1.2 DataBinding给Adapter带来的改变不只是少写setTextDataBinding解决的事情比“少写几行setText”要深刻一些。它把布局和数据的对应关系从代码挪到了XML声明里让item的UI结构在布局文件里就能看出来。以前写列表item要先在XML里给每个View设置id再去Activity或Adapter里findViewById然后一行行setText。数据字段多的时候一个item有七八个字段onBindViewHolder里就是一大段赋值代码。用DataBinding之后item布局长这样layout xmlns:androidhttp://schemas.android.com/apk/res/android data variable nameitem typecom.example.model.MessageItem / /data LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding12dp TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text{item.title} android:textSize16sp android:textStylebold / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text{item.content} / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text{item.time} android:textSize12sp / /LinearLayout /layout然后在Adapter里把MessageBinding和MessageItem一绑赋值逻辑全部由DataBinding自动完成。这不只是少写了setText更是把UI怎么渲染交给了声明式框架代码里只需要关心数据本身。我自己的感受是切到DataBinding之后新增字段的成本明显下降只要在数据类里加字段布局里声明对应的TextView不用再动Adapter代码。这是传统写法做不到的。2. 基类设计把稳定和变化拆开2.1 泛型基类的核心设计思路基类管流程子类管绑定设计Adapter基类之前先想清楚一个问题哪些东西是每个Adapter都一样的哪些是每个Adapter不一样的。一样的东西是框架流程创建ViewHolder、绑定ViewHolder、返回itemCount。这几个方法是RecyclerView的适配器接口要求必须实现的属于雷打不动的模板代码。不一样的东西是item布局是哪个、数据类是什么、item上的数据怎么绑定到布局上。这三样东西必须由子类提供。于是基类的设计骨架就很清晰了abstract class BaseBindingAdapterVB : ViewDataBinding, T( LayoutRes protected val layoutResId: Int ) : RecyclerView.AdapterBaseBindingAdapter.BindingHolderVB()泛型参数一个是VB一个是T。VB是DataBinding根据布局生成的Binding类型T是item的数据类型。子类只需要继承并实现一个convert方法override fun convert(binding: ItemMessageBinding, item: MessageItem, position: Int) { binding.item item }这个设计的优点是把“流程”和“业务”彻底分开了。基类负责RecyclerView的回调、ViewHolder的持有和复用、Binding的创建子类只关心item数据怎么绑定到Binding对象上。职责单一改动风险小。2.2 为什么使用布局id传入而不是泛型反射有人可能会问为什么不在基类里通过泛型VB反射获取布局然后直接inflate这样连layoutResId都不用传这个做法技术上是能实现的通过反射拿Binding类里的静态inflate方法但我不建议在主项目里这么干。原因有几个。第一反射在Android上有性能和混淆双重风险虽然viewBinding生成的类不会被混淆但反射代码在排查问题时定位困难第二DataBindingUtil.inflate本身只需要一个布局id传进去是最直接、最可控的方式第三多类型列表下布局id本来就是动态的反射方案完全不支持这种场景。我的原则是能用显式参数解决的问题不用反射能写清楚的逻辑不搞黑魔法。开发效率不差这一丁点代码量但是出bug排错的时间差距很大。2.3 多类型布局的抽象策略从单一泛型退一步多类型列表和单类型列表有一个本质冲突不同item布局生成不同的Binding类型而单个Adapter只能持有一组ViewHolder类型。理想情况是让每个item类型都有自己的Binding类型但这样做基类的泛型设计会非常复杂。实际工程中最实用的方案是多类型基类里统一使用ViewDataBinding作为Binding类型它是所有DataBinding生成类的基类。由于每个item类型都对应不同的布局idDataBindingUtil.inflate返回什么Binding类型在运行期是确定的子类在convert方法里只需要根据viewType把ViewDataBinding强转成具体的Binding类型。abstract class MultiBindingAdapterT( private val layoutProvider: (viewType: Int) - Int ) : RecyclerView.AdapterMultiBindingAdapter.BindingHolder() { // 具体实现见下一节 }这是典型的“退一步”设计既然多类型无法用强类型泛型约束那就退到基类类型用文档和规范约束子类的强转行为。牺牲一点点类型安全换来代码结构的统一。3. 代码实现单类型与多类型两个版本3.1 准备工作build.gradle配置和布局改造使用这套基类之前需要把DataBinding开关打开。在模块的build.gradle里配置android { buildFeatures { dataBinding true } }注意如果是老版本Gradle用的是dataBinding { enabled true }这种写法新版本统一推荐buildFeatures.dataBinding true。开启之后项目里的XML布局如果根节点是layout就会自动生成对应的Binding类。比如item_message.xml生成ItemMessageBinding命名规则是下划线转驼峰加Binding后缀。还有一类情况是项目里只开了ViewBinding没用DataBinding。ViewBinding生成的类也有inflate方法但不会生成data变量声明能力。我的这套基类核心依赖的是DataBinding因为convert里绑定数据需要用到data里声明的variable。如果只是ViewBindingconvert只能拿到View引用绑定逻辑又回到了findViewById没有意义。3.2 单类型BaseBindingAdapter完整代码这是基类的最基础版本支持单类型item提供设置数据、追加数据、获取item等方法abstract class BaseBindingAdapterVB : ViewDataBinding, T( LayoutRes protected val layoutResId: Int ) : RecyclerView.AdapterBaseBindingAdapter.BindingHolderVB() { private val items mutableListOfT() fun setData(newData: ListT?) { items.clear() if (!newData.isNullOrEmpty()) { items.addAll(newData) } notifyDataSetChanged() } fun addData(data: ListT?) { if (data.isNullOrEmpty()) return val startPosition items.size items.addAll(data) notifyItemRangeInserted(startPosition, data.size) } fun getItem(position: Int): T items[position] class BindingHolderVB : ViewDataBinding( val binding: VB ) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolderVB { val binding DataBindingUtil.inflateVB( LayoutInflater.from(parent.context), layoutResId, parent, false ) return BindingHolder(binding) } override fun onBindViewHolder(holder: BindingHolderVB, position: Int) { val binding holder.binding convert(binding, getItem(position), position) binding.executePendingBindings() } override fun getItemCount(): Int items.size abstract fun convert(binding: VB, item: T, position: Int) }几个细节值得重点说明。第一onCreateViewHolder里inflate参数必须传parent和false。传true会导致item直接添加到RecyclerView的父容器上运行时会抛IllegalStateException。第二onBindViewHolder里我把executePendingBindings放在convert之后这个方法的含义是立即执行挂起的绑定任务。DataBinding默认会在下一帧刷新UI但在RecyclerView滚动场景下item复用速度很快延迟一帧可能造成内容闪烁所以手动调用强制同步刷新。第三BindingHolder持有binding对象binding.root就是item的根View直接传给ViewHolder做itemView使用。3.3 多类型MultiBindingAdapter完整代码多类型版的设计核心是layout provider。子类根据viewType返回对应的布局id基类负责inflate并统一以ViewDataBinding类型返回abstract class MultiBindingAdapterT( private val layoutProvider: (viewType: Int) - Int ) : RecyclerView.AdapterMultiBindingAdapter.BindingHolder() { private val items mutableListOfT() fun setData(newData: ListT?) { items.clear() if (!newData.isNullOrEmpty()) { items.addAll(newData) } notifyDataSetChanged() } class BindingHolder( val binding: ViewDataBinding ) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val layoutResId layoutProvider.invoke(viewType) val binding DataBindingUtil.inflateViewDataBinding( LayoutInflater.from(parent.context), layoutResId, parent, false ) return BindingHolder(binding) } override fun onBindViewHolder(holder: BindingHolder, position: Int) { val item items[position] convert(holder.binding, item, position) holder.binding.executePendingBindings() } override fun getItemViewType(position: Int): Int getItemType(position) override fun getItemCount(): Int items.size abstract fun getItemType(position: Int): Int abstract fun convert(binding: ViewDataBinding, item: T, position: Int) }使用的时候子类需要做两件事通过getItemType告诉Adapter当前item是哪种类型在convert里通过viewType或item类型把ViewDataBinding强转成具体Bindingclass MessageAdapter : MultiBindingAdapterMessageItem( layoutProvider { viewType - when (viewType) { TYPE_TEXT - R.layout.item_message_text TYPE_IMAGE - R.layout.item_message_image else - throw IllegalArgumentException(unknown viewType $viewType) } } ) { override fun getItemType(position: Int): Int { return when (getItem(position)) { is TextMessage - TYPE_TEXT is ImageMessage - TYPE_IMAGE else - TYPE_UNKNOWN } } override fun convert(binding: ViewDataBinding, item: MessageItem, position: Int) { when (binding) { is ItemMessageTextBinding - binding.msg item as TextMessage is ItemMessageImageBinding - binding.msg item as ImageMessage } } companion object { const val TYPE_TEXT 0 const val TYPE_IMAGE 1 const val TYPE_UNKNOWN -1 } }这种写法在多类型场景下非常实用。它不像网上流传的一些复杂方案那样引入大量抽象类而是直接利用ViewDataBinding基类做统一入口子类里一个when就搞定绑定逻辑清晰直观。3.4 加入DiffUtil让数据刷新从全量变精准前面两个版本的setData方法都用了notifyDataSetChanged这个方法的性能问题在列表数据量小的时候不明显一超过几百条就非常浪费。RecyclerView会把所有可见item全部重新绑定即使只有一条数据变了。DiffUtil就是为了解决这个问题而生的。我们可以在基类里增加一个支持DiffUtil的submitList版本核心逻辑如下abstract class DiffBindingAdapterVB : ViewDataBinding, T( LayoutRes protected val layoutResId: Int, private val diffCallback: DiffUtil.ItemCallbackT ) : RecyclerView.AdapterDiffBindingAdapter.BindingHolderVB() { private var items emptyListT() fun submitList(newList: ListT) { val oldList items val result DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize(): Int oldList.size override fun getNewListSize(): Int newList.size override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { return diffCallback.areItemsTheSame(oldItem, newItem) } override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { return diffCallback.areContentsTheSame(oldItem, newItem) } }) items newList result.dispatchUpdatesTo(this) } // 其余结构与 BaseBindingAdapter 相同 abstract fun convert(binding: VB, item: T, position: Int) }DiffUtil的核心是areItemsTheSame和areContentsTheSame这两个方法。areItemsTheSame判断两个item是否代表同一条数据通常用id对比areContentsTheSame判断同一条数据的内容是否发生变化通常用equals或者逐字段对比。它们的分工决定了Diff算法能定位到“哪条item变了、哪个item插入了、哪个item被删除了”从而精确调用notifyItemChanged、notifyItemInserted、notifyItemRemoved这些方法。原理层面的理解很重要DiffUtil并不是检测UI变化而是基于两个列表的新旧快照做线性差异计算。它会在后台线程算出差异结果然后一次性派发到RecyclerView整个过程不重新创建ViewHolder只是复用现有item做增量更新。这也是为什么配合DataBinding时刷新效率很高某一条item内容变化时RecyclerView只回调那一行的onBindViewHolder重新给Binding赋一次值就完成了。如果列表数据量极大比如上千条直接在主线程调用DiffUtil.calculateDiff仍然可能造成卡顿因为计算本身是有开销的。这种情况建议直接用androidx里的ListAdapter它内部封装了AsyncListDiffer会把Diff计算自动放到后台线程。基类和ListAdapter的封装思想一致项目里如果已经用了ListAdapter也可以在onBindViewHolder里获取ItemListAdapter的Binding对象只是代码会绕一些。我的建议是数据量小几百条以内直接用本文的方案数据量大再考虑ListAdapter。4. 一个真实的业务接入Demo4.1 数据类与item布局准备做一个模拟的消息列表数据模型有三种类型文本消息、图片消息、系统通知。为了演示多类型基类我用前文的MultiBindingAdapter实现sealed class MessageItem { data class TextMessage( val id: Long, val title: String, val content: String, val time: Long ) : MessageItem() data class ImageMessage( val id: Long, val imageUrl: String, val summary: String, val time: Long ) : MessageItem() data class SystemMessage( val id: Long, val tips: String, val time: Long ) : MessageItem() }布局文件对应三个item_message_text.xml、item_message_image.xml、item_message_system.xml。每个布局根节点都是layout内部声明对应的variable。拿文本消息举例layout xmlns:androidhttp://schemas.android.com/apk/res/android data variable namemsg typecom.example.demo.MessageItem.TextMessage / /data LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding12dp TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text{msg.title} android:textSize15sp android:textStylebold / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text{msg.content} / /LinearLayout /layout需要注意一个细节variable里的type要写全限定名不能只写类名否则编译期报错找不到类。这个错误报得很隐晦IDE只在运行期才会暴露问题写的时候一定注意。4.2 页面里的接入代码Activity或Fragment里接入步骤非常简洁class MessageListFragment : Fragment() { private lateinit var adapter: MessageAdapter override fun onCreateView(...): View? { binding FragmentMessageListBinding.inflate(inflater) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { adapter MessageAdapter() binding.recyclerView.layoutManager LinearLayoutManager(requireContext()) binding.recyclerView.adapter adapter val list loadMockData() adapter.setData(list) } private fun loadMockData(): ListMessageItem { // mock 3条文本、2条图片、1条系统消息 } }整个页面代码不需要处理任何ViewHolder相关逻辑因为Adapter基类已经全部接管了。调用setData之后列表自动刷新数据的加载、分页、下拉刷新都只需要替换List对象。4.3 事件绑定的三种方式对比列表页几乎都少不了点击事件。在DataBinding Adapter基类的架构下事件绑定我用过三种方案各有适用场景。方案一在convert里直接给View设置点击监听。override fun convert(binding: ItemMessageBinding, item: MessageItem.TextMessage, position: Int) { binding.msg item binding.root.setOnClickListener { onMessageClick?.invoke(item, it, position) } }这个方案最简单适合只做整item点击不需要传额外参数的情况。注意position要用holder的getBindingAdapterPosition()获取不要在onBindViewHolder里直接使用参数position。方案二在item布局的XML里使用DataBinding的lambda表达式绑定回调。data variable nameitem typecom.example.demo.MessageItem.TextMessage / variable namecallback typecom.example.demo.MessageItemClickListener / /data LinearLayout android:onClick{(view) - callback.onTextClick(item, view)} ... /这个方案把点击行为完全声明式化XML里就能看到哪个View绑定到哪个回调代码里只需要给binding赋值时把callback传进去。适合item里有多个控件各自响应不同点击的复杂场景。方案三使用BindingAdapter自定义点击事件绑定。BindingAdapter(onItemClick) fun View.setOnItemClick(listener: View.OnClickListener?) { setOnClickListener(listener) }然后用app:onItemClick在XML里绑定。这个方案适合需要对点击逻辑做统一增强的情况比如防重复点击、上报点击埋点。但大多数场景用前两种就够了不建议为了一个简单的点击事件把逻辑藏到三方文件里。我实际项目里用的是方案一加方案二的混合整item点击走convertitem内部不同控件点击走XML绑定。各自职责清晰排查问题也方便。5. 测试和上线后踩过的坑与排查记录5.1 列表不刷新或刷新闪烁DataBinding的刷新时机这个坑几乎是必踩的。刚封装好基类时我在一个页面里发现下拉刷新后列表内容有时候不更新有时候更新了却闪一下特别是快速滑动的时候。排查后发现根因有两个。第一个是没有在onBindViewHolder里调用executePendingBindings。因为RecyclerView复用的是ViewHolderbinding对象是同一个如果这次赋值的数据没触发立即刷新item上显示的还是上一个数据的内容。我在基类里加了binding.executePendingBindings()之后这个问题就消失了。第二个原因更隐蔽如果在onBindViewHolder里给binding的variable赋了新值但对应的数据类没有正确实现equals/hashCodeDataBinding内部对表达式条件依赖的缓存判断就会误判导致应该刷新的UI没有刷新。这种情况常见于数据类改动后没有重建hashCode。排查时先检查数据类的equals实现再看executePendingBindings基本能覆盖90%的问题。5.2 数据更新越界崩溃setData的线程安全问题还有一次线上崩溃是IndexOutOfBoundsException定位到Adapter里的items集合。崩溃原因是一个页面在收到推送后直接调setData但同时RecyclerView正在滚动UI线程上执行notifyDataSetChanged而另一条线程刚好在addData两个方法对同一个MutableList做读写就会把内部数组的modCount和size搞乱。解决方法是给数据更新的入口统一加同步锁或者更推荐的做法是所有数据更新都在主线程执行网络请求结果回来后通过主线程Handler或协程Main调度再调用setData。我在基类里也做了防御处理setData内部用synchronized修饰避免在极端情况下出现数据竞争。这个坑提示一个普遍规律Adapter的items集合是UI状态的一部分任何线程对它的修改都会影响RecyclerView的渲染。多线程场景务必收敛到主线程更新。5.3 局部刷新Payload一帧刷一行的正确姿势列表里经常有“点赞后只更新点赞数”的场景。如果在业务侧直接调notifyItemChanged(position)不带payloadRecyclerView会走完整的onBindViewHolder浪费资源。更好的做法是使用payload机制在基类里重载onBindViewHolder的payload版本override fun onBindViewHolder( holder: BindingHolderVB, position: Int, payloads: MutableListAny ) { if (payloads.isEmpty()) { onBindViewHolder(holder, position) } else { convertPayload(holder.binding, getItem(position), payloads, position) holder.binding.executePendingBindings() } } open fun convertPayload(binding: VB, item: T, payloads: ListAny, position: Int) { // 子类选择性实现 }业务侧更新点赞数时调用adapter.notifyItemChanged(position, like_count)同时在子类里重写convertPayload只更新点赞相关的TextView。这样一行刷新只改动一个Binding属性UI上的其余部分完全不重新绑定滑动更流畅代码也不会写很乱。5.4 XML空值和类型不匹配的崩溃预防用DataBinding在XML里写表达式时TextView的text如果绑定了一个null字段默认会渲染成空字符串不会崩溃。但如果是绑定了int类型到android:visibility这种属性或者绑定的表达式返回类型和属性要求的类型不一致编译期不报错运行期就可能抛异常。我的习惯是XML表达式里尽量写空安全调用{item?.title}而不是{item.title}尤其当item对象本身可能为null时。DataBinding生成的代码会直接调用item.titleNPE就发生了。另外就是BindingAdapter的返回值类型要严格匹配比如给ImageView写了一个返回Int的BindingAdapter但XML里用在了app:placeHolder上ImageView的placeHolder属性要求Drawable运行时就会报类型转换异常。这类问题在开发阶段用真机测一下通常都能发现。5.5 与ViewBinding混用时的一个小警告有些项目里既开了ViewBinding又开了DataBinding两者生成的Binding类名可能完全相同。比如布局item_message.xmlViewBinding生成ItemMessageBindingDataBinding也生成ItemMessageBinding但包路径不同。IDE的自动导入如果选错编译能通过运行期inflate就会报错或者拿不到Binding对象。这类问题排查起来很头疼因为编译器和IDE都不会明确提示。我的经验是项目里统一用一个只用DataBindingViewBinding开关不打开或者只在fine-grained粒度的局部模块用ViewBinding整库不混用。避免同名的Binding类同时存在是最省心的方式。5.6 大列表卡顿的真实测试记录最后说一个性能层面的实测结论。我用同一个模拟列表50条、500条、2000条数据分别用setDatanotifyDataSetChanged和submitListDiffUtil跑过一轮对比。50条数据感知不到差别500条时setData在部分中低端机器上能感觉到卡顿submitList基本流畅2000条时setData滚动时掉帧明显submitList的Diff计算时间大约在几十毫秒量级UI无明显卡顿。所以最终封装时超过200条数据的列表我统一采用了DiffBindingAdapter版本配合业务的刷新频率体验提升还是很明显的。如果你的列表数据量长期保持在200条以内用最基础的BaseBindingAdapter就足够了不必为了DiffUtil引入多余的计算开销。写在最后的一点体会这个基类我用了将近两年中间迭代过三版从最初只有单类型到后来支持多类型再到引入DiffUtil整体方向一直没有变过把Adapter里那些和业务无关的框架代码收敛到一处。实际收益比我预期更大一个从旧项目抄过来的十几行Adapter现在压缩到三五行列表页新增一个接口只需要写数据和布局Adapter代码基本不动。如果项目里现在还在大量手写ViewHolder建议先找一个模块试着切到DataBinding和这套基类感受一下工作量的变化。首次改造有一定成本因为布局根节点必须改成layout、老代码里的findViewById要全部删掉但改造完成之后后面每个新增列表页都是纯增量收益。调试工具上推荐在布局里多用{...}表达式之前先写一个静态值验证显示效果能节省不少时间。一个小技巧基类里还可以顺手把点击防重的逻辑加上比如在convert里对itemView设置一个全局的点击拦截器用时间戳差过滤1秒内的重复点击。这种跨业务通用的逻辑放基类里一次解决比每个Adapter各写一遍高效得多。列表页开发本质上没有太多高深的东西把重复的代码收敛住把差异的部分留清楚剩下的都是流畅的日常。