news 2026/10/17 1:11:51

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于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长什么样。假设是一个消息列表,数据类是MessageItem,item布局是item_message.xml。很多人项目里现在还是这么写的:

class MessageAdapter( private val items: MutableList<MessageItem> ) : RecyclerView.Adapter<MessageAdapter.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带来的改变不只是少写setText

DataBinding解决的事情比“少写几行setText”要深刻一些。它把布局和数据的对应关系从代码挪到了XML声明里,让item的UI结构在布局文件里就能看出来。

以前写列表item,要先在XML里给每个View设置id,再去Activity或Adapter里findViewById,然后一行行setText。数据字段多的时候,一个item有七八个字段,onBindViewHolder里就是一大段赋值代码。

用DataBinding之后,item布局长这样:

<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="item" type="com.example.model.MessageItem" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{item.title}" android:textSize="16sp" android:textStyle="bold" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{item.content}" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{item.time}" android:textSize="12sp" /> </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 BaseBindingAdapter<VB : ViewDataBinding, T>( @LayoutRes protected val layoutResId: Int ) : RecyclerView.Adapter<BaseBindingAdapter.BindingHolder<VB>>()

泛型参数一个是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类型都对应不同的布局id,DataBindingUtil.inflate返回什么Binding类型,在运行期是确定的,子类在convert方法里只需要根据viewType把ViewDataBinding强转成具体的Binding类型。

abstract class MultiBindingAdapter<T>( private val layoutProvider: (viewType: Int) -> Int ) : RecyclerView.Adapter<MultiBindingAdapter.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。如果只是ViewBinding,convert只能拿到View引用,绑定逻辑又回到了findViewById,没有意义。

3.2 单类型BaseBindingAdapter完整代码

这是基类的最基础版本,支持单类型item,提供设置数据、追加数据、获取item等方法:

abstract class BaseBindingAdapter<VB : ViewDataBinding, T>( @LayoutRes protected val layoutResId: Int ) : RecyclerView.Adapter<BaseBindingAdapter.BindingHolder<VB>>() { private val items = mutableListOf<T>() fun setData(newData: List<T>?) { items.clear() if (!newData.isNullOrEmpty()) { items.addAll(newData) } notifyDataSetChanged() } fun addData(data: List<T>?) { if (data.isNullOrEmpty()) return val startPosition = items.size items.addAll(data) notifyItemRangeInserted(startPosition, data.size) } fun getItem(position: Int): T = items[position] class BindingHolder<VB : ViewDataBinding>( val binding: VB ) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<VB> { val binding = DataBindingUtil.inflate<VB>( LayoutInflater.from(parent.context), layoutResId, parent, false ) return BindingHolder(binding) } override fun onBindViewHolder(holder: BindingHolder<VB>, 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 MultiBindingAdapter<T>( private val layoutProvider: (viewType: Int) -> Int ) : RecyclerView.Adapter<MultiBindingAdapter.BindingHolder>() { private val items = mutableListOf<T>() fun setData(newData: List<T>?) { 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.inflate<ViewDataBinding>( 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强转成具体Binding:

class MessageAdapter : MultiBindingAdapter<MessageItem>( 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 DiffBindingAdapter<VB : ViewDataBinding, T>( @LayoutRes protected val layoutResId: Int, private val diffCallback: DiffUtil.ItemCallback<T> ) : RecyclerView.Adapter<DiffBindingAdapter.BindingHolder<VB>>() { private var items = emptyList<T>() fun submitList(newList: List<T>) { 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. 一个真实的业务接入Demo

4.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:android="http://schemas.android.com/apk/res/android"> <data> <variable name="msg" type="com.example.demo.MessageItem.TextMessage" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{msg.title}" android:textSize="15sp" android:textStyle="bold" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_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(): List<MessageItem> { // 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 name="item" type="com.example.demo.MessageItem.TextMessage" /> <variable name="callback" type="com.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点击走convert,item内部不同控件点击走XML绑定。各自职责清晰,排查问题也方便。

5. 测试和上线后踩过的坑与排查记录

5.1 列表不刷新或刷新闪烁:DataBinding的刷新时机

这个坑几乎是必踩的。刚封装好基类时,我在一个页面里发现下拉刷新后列表内容有时候不更新,有时候更新了却闪一下,特别是快速滑动的时候。

排查后发现根因有两个。

第一个是没有在onBindViewHolder里调用executePendingBindings。因为RecyclerView复用的是ViewHolder,binding对象是同一个,如果这次赋值的数据没触发立即刷新,item上显示的还是上一个数据的内容。我在基类里加了binding.executePendingBindings()之后这个问题就消失了。

第二个原因更隐蔽:如果在onBindViewHolder里给binding的variable赋了新值,但对应的数据类没有正确实现equals/hashCode,DataBinding内部对表达式条件依赖的缓存判断就会误判,导致应该刷新的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)不带payload,RecyclerView会走完整的onBindViewHolder,浪费资源。更好的做法是使用payload机制,在基类里重载onBindViewHolder的payload版本:

override fun onBindViewHolder( holder: BindingHolder<VB>, position: Int, payloads: MutableList<Any> ) { 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: List<Any>, 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.title,NPE就发生了。另外就是BindingAdapter的返回值类型要严格匹配,比如给ImageView写了一个返回Int的BindingAdapter,但XML里用在了app:placeHolder上,ImageView的placeHolder属性要求Drawable,运行时就会报类型转换异常。这类问题在开发阶段用真机测一下通常都能发现。

5.5 与ViewBinding混用时的一个小警告

有些项目里既开了ViewBinding又开了DataBinding,两者生成的Binding类名可能完全相同。比如布局item_message.xml,ViewBinding生成ItemMessageBinding,DataBinding也生成ItemMessageBinding,但包路径不同。IDE的自动导入如果选错,编译能通过,运行期inflate就会报错或者拿不到Binding对象。

这类问题排查起来很头疼,因为编译器和IDE都不会明确提示。我的经验是项目里统一用一个:只用DataBinding,ViewBinding开关不打开;或者只在fine-grained粒度的局部模块用ViewBinding,整库不混用。避免同名的Binding类同时存在,是最省心的方式。

5.6 大列表卡顿的真实测试记录

最后说一个性能层面的实测结论。我用同一个模拟列表,50条、500条、2000条数据,分别用setData(notifyDataSetChanged)和submitList(DiffUtil)跑过一轮对比。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各写一遍高效得多。列表页开发本质上没有太多高深的东西,把重复的代码收敛住,把差异的部分留清楚,剩下的都是流畅的日常。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 1:11:49

POJ 1700快速渡河问题:贪心策略经典解析与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:11:25

FastAdmin 实战避坑手册:权限、CRUD、API 接口高频问题全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:11:03

基于MP2642GR-Z与TM4C1294的被动均衡BMS设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:10:38

豆瓣Top250爬虫实战:从请求伪装到数据落库的完整工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:10:37

两串锂电池被动均衡方案:MP2642GR-Z与STM32L432KC实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:10:34

两串电芯被动均衡方案:MP2642GR-Z与MKV46F128VLH16协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华