如何为DiffUtil重构密封类?多态方案的优劣与实现指引
把DiffUtil比较逻辑内置于Item的方案分析、实现与替代思路
这个方案是否更优?
直接拆解优缺点:
- 好处:
- 贴合单一职责:每个列表项自己负责判断自身的同一性,DiffCallback不用堆一堆when分支判断类型,代码清爽很多,后续加新类型也不用改Callback,符合开闭原则。
- 逻辑内聚:和Item相关的比较逻辑跟着Item走,找问题的时候不用在多个文件间跳来跳去。
- 坏处:
- 会让Item类变“重”:如果比较逻辑复杂(比如要判断多个字段或者有特殊规则),Item就不再是纯粹的数据载体,混进了业务逻辑。
- 复杂刷新场景不好维护:如果要做局部刷新的payload,把逻辑拆在每个Item里,后期调整起来可能比集中在Callback里麻烦。
正确实现步骤
先定义一个通用的ListType接口,把DiffUtil需要的核心方法抽象出来:
interface ListType { // 判断是否是同一个条目(对应areItemsTheSame) fun isSameItem(other: ListType): Boolean // 判断内容是否一致(对应areContentsTheSame) fun isSameContent(other: ListType): Boolean // 可选:返回局部刷新的payload,默认返回null fun getChangePayload(other: ListType): Any? = null }
然后让你的密封类所有子类实现这个接口,利用智能转换做类型安全判断:
sealed class StatusListData : ListType { data class TextStatus(val id: String, val content: String) : StatusListData() { override fun isSameItem(other: ListType): Boolean { // 先判断类型匹配,再对比唯一标识 return other is TextStatus && this.id == other.id } override fun isSameContent(other: ListType): Boolean { return other is TextStatus && this.content == other.content } override fun getChangePayload(other: ListType): Any? { if (other !is TextStatus) return null // 返回变化字段标识,用于局部刷新 return if (content != other.content) "TEXT_CONTENT_UPDATED" else null } } data class ImageStatus(val imageId: String, val url: String, val width: Int) : StatusListData() { override fun isSameItem(other: ListType): Boolean { return other is ImageStatus && this.imageId == other.imageId } override fun isSameContent(other: ListType): Boolean { return other is ImageStatus && this.url == other.url && this.width == other.width } } }
最后简化DiffCallback,直接调用Item的接口方法即可:
class StatusListDiffCallback( private val oldList: List<ListType>, private val newList: List<ListType> ) : DiffUtil.Callback() { override fun getOldListSize() = oldList.size override fun getNewListSize() = newList.size override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean { return oldList[oldPos].isSameItem(newList[newPos]) } override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean { return oldList[oldPos].isSameContent(newList[newPos]) } override fun getChangePayload(oldPos: Int, newPos: Int): Any? { return oldList[oldPos].getChangePayload(newList[newPos]) } }
关于类型转换的问题
你担心的类型转换风险,用is关键字的智能转换就能解决——在接口方法里先判断other是否是当前子类的实例,再做比较,完全不会出现ClassCastException。如果想偷懒,还可以给接口加默认实现,避免每个子类重复写类型判断:
interface ListType { fun isSameItem(other: ListType): Boolean { // 默认逻辑:类型相同且equals相等(data class默认实现了equals) return this::class == other::class && this == other } fun isSameContent(other: ListType): Boolean { return this == other } }
这样子类只需要在有特殊判断逻辑的时候重写方法就行,比如只对比id而不是所有字段的场景。
更合适的替代方案
方案1:用扩展函数封装比较逻辑
如果不想让Item实现接口,破坏数据类的纯粹性,可以给密封类加扩展函数,把比较逻辑集中起来:
// 给StatusListData添加扩展函数,统一处理比较逻辑 fun StatusListData.isSameItem(other: StatusListData): Boolean { return when (this) { is StatusListData.TextStatus -> other is StatusListData.TextStatus && this.id == other.id is StatusListData.ImageStatus -> other is StatusListData.ImageStatus && this.imageId == other.imageId } } fun StatusListData.isSameContent(other: StatusListData): Boolean { return when (this) { is StatusListData.TextStatus -> other is StatusListData.TextStatus && this.content == other.content is StatusListData.ImageStatus -> other is StatusListData.ImageStatus && this.url == other.url && this.width == other.width } }
然后DiffCallback里直接调用扩展函数:
class StatusListDiffCallback( private val oldList: List<StatusListData>, private val newList: List<StatusListData> ) : DiffUtil.Callback() { override fun getOldListSize() = oldList.size override fun getNewListSize() = newList.size override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean { return oldList[oldPos].isSameItem(newList[newPos]) } override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean { return oldList[oldPos].isSameContent(newList[newPos]) } }
这种方式既保持了Item的纯粹性,又避免了DiffCallback里的冗长when分支。
方案2:用官方推荐的DiffUtil.ItemCallback
如果用的是RecyclerView的ListAdapter,官方更推荐用DiffUtil.ItemCallback,结合密封类的when分支,代码简洁且逻辑集中,便于维护:
val StatusItemCallback = object : DiffUtil.ItemCallback<StatusListData>() { override fun areItemsTheSame(oldItem: StatusListData, newItem: StatusListData): Boolean { return when (oldItem) { is StatusListData.TextStatus -> newItem is StatusListData.TextStatus && oldItem.id == newItem.id is StatusListData.ImageStatus -> newItem is StatusListData.ImageStatus && oldItem.imageId == newItem.imageId } } override fun areContentsTheSame(oldItem: StatusListData, newItem: StatusListData): Boolean { // data class默认equals会对比所有字段,直接用就行 return oldItem == newItem } override fun getChangePayload(oldItem: StatusListData, newItem: StatusListData): Any? { return when (oldItem) { is StatusListData.TextStatus -> { newItem as StatusListData.TextStatus if (oldItem.content != newItem.content) mapOf("content" to newItem.content) else null } is StatusListData.ImageStatus -> { newItem as StatusListData.ImageStatus val changes = mutableMapOf<String, Any>() if (oldItem.url != newItem.url) changes["url"] = newItem.url if (oldItem.width != newItem.width) changes["width"] = newItem.width changes.takeIf { it.isNotEmpty() } } } } }
这种方式的好处是逻辑全在Callback里,Item还是纯数据类,适合大多数场景,尤其是比较逻辑不算特别复杂的情况。
内容的提问来源于stack exchange,提问作者c-an
相关产品推荐
相关产品推荐

