You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 04:54:24