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

Android中RecyclerView结合DiffUtils滚动条目错乱问题解决方案

问题根源及修复方案

1. 修复DiffUtils核心逻辑

你的BaseDiffUtils完全依赖引用相等(===)判断条目是否相同,这在数据更新时(比如网络请求返回新的Result对象,即使内容一致)会被判定为不同条目,直接导致DiffUtils失效,进而引发视图复用混乱。

修改BaseDiffUtils:

  • areItemsTheSame:用数据的唯一标识判断(比如Result的id字段,替换成你实际的唯一主键)
  • areContentsTheSame:用内容相等判断(如果Result是数据类,直接用==;否则手动对比所有影响UI的字段)
class BaseDiffUtils<T>(private val oldItem: List<T>, private val newItem: List<T>) : DiffUtil.Callback() {
    override fun getOldListSize(): Int = oldItem.size
    override fun getNewListSize(): Int = newItem.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val old = oldItem[oldItemPosition] as Result
        val new = newItem[newItemPosition] as Result
        return old.id == new.id // 用唯一ID判断是否为同一个条目
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val old = oldItem[oldItemPosition] as Result
        val new = newItem[newItemPosition] as Result
        // 数据类直接用==,非数据类则手动对比所有UI相关字段
        return old == new
    }
}

2. 修复Adapter的ViewHolder绑定问题

你的Adapter将binding定义为全局变量,每次创建ViewHolder都会覆盖这个全局实例,导致所有ViewHolder共享同一个绑定对象,这是视图复用错乱的核心原因之一。

修改ReservedListAdapter:

  • 将binding移到ViewHolder内部,每个ViewHolder持有独立的ItemBinding
  • 移除holder.setIsRecyclable(false),恢复视图复用
class ReservedListAdapter @Inject constructor() : RecyclerView.Adapter<ReservedListAdapter.ViewHolder>() {

    private var moviesList = emptyList<Result>()
    private val userToken by lazy { GoodPrefs.getInstance().getString(USER_TOKEN, "") }
    private var selectedType = ""

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemReservedListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding) // 将binding传入ViewHolder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(moviesList[position])
        // 移除holder.setIsRecyclable(false)
    }

    override fun getItemCount() = moviesList.size

    // 开启getItemId,配合DiffUtils提升复用稳定性
    override fun getItemId(position: Int): Long {
        return moviesList[position].id.toLong() // 替换成你的唯一ID字段
    }

    // ViewHolder持有独立的binding实例
    inner class ViewHolder(private val binding: ItemReservedListBinding) : RecyclerView.ViewHolder(binding.root) {

        @SuppressLint("SetTextI18n", "CheckResult")
        fun bind(item: Result) {
            binding.apply {
                // Title
                var type = ""
                item.counseling?.let { counseling ->
                    type = if (counseling.type == PHONE) {
                        itemView.context.getString(R.string.byPhone)
                    } else {
                        itemView.context.getString(R.string.textual)
                    }
                    titleTxt.text = "${itemView.context.getString(R.string.visit)} $type - " +
                            itemView.context.getString(R.string.doctor) +
                            "${counseling.physician?.firstName} ${counseling.physician?.lastName}"
                } ?: run {
                    // 处理counseling为null的情况,清空旧数据
                    titleTxt.text = ""
                }

                // Date
                if (item.startAt != null) {
                    val date = item.startAt.split(" ")[0]
                    val hour = item.startAt.split(" ")[1]
                    val showDate = if (item.counseling?.type == PHONE) {
                        "${date.convertDateToFarsiWithMonthName()} | " + "${itemView.context.getString(R.string.hour)} ${hour.dropLast(3)}"
                    } else {
                        "${date.convertDateToFarsiWithMonthName()} | ${itemView.context.getString(R.string.duringDay)}"
                    }
                    dateTxt.text = showDate
                } else {
                    // 处理startAt为null的情况,清空旧数据
                    dateTxt.text = ""
                }

                // Avatar - 关键优化:先重置默认图,取消旧请求
                avatarImg.load(R.drawable.avatar_doctor)
                avatarLoading.visibility = View.VISIBLE
                var avatarFound = false

                item.counseling?.physician?.profile?.files?.let { files ->
                    if (files.isNotEmpty()) {
                        files.forEach { file ->
                            if (file?.description == AVATAR) {
                                avatarFound = true
                                // 用Disposable管理请求,防止旧请求覆盖新数据
                                val disposable = ApiClient.getInstance().apisUseCase().getAvatarImage(userToken, file.id!!)
                                    .applyIoScheduler()
                                    .subscribe({
                                        avatarLoading.visibility = View.GONE
                                        if (it.isSuccessful && it.code() == 200 && it.body()?.data?.file != null) {
                                            val decodedBytes: ByteArray = Base64.decode(it.body()!!.data!!.file, Base64.DEFAULT)
                                            avatarImg.load(decodedBytes)
                                        }
                                    }, {
                                        avatarLoading.visibility = View.GONE
                                    })
                                itemView.tag = disposable
                            }
                        }
                    }
                }

                if (!avatarFound) {
                    avatarLoading.visibility = View.GONE
                }

                // Status
                infoTxt.text = item.status ?: ""
                infoTxt.setCorner(10)

                // Click - 移除notifyDataSetChanged,避免全局刷新
                binding.root.setOnClickListener {
                    onItemClickListener?.let { listener ->
                        listener(item, selectedType)
                    }
                }
            }
        }

        // 回收时取消未完成的网络请求
        override fun onViewRecycled() {
            super.onViewRecycled()
            val disposable = itemView.tag as? Disposable
            disposable?.dispose()
        }
    }

    // 修改方法,使用ViewHolder的binding
    private fun dynamicallyStatusColor(binding: ItemReservedListBinding, textColor: Int, bgColor: Int, context: Context) {
        binding.infoTxt.apply {
            setTextColor(ContextCompat.getColor(context, textColor))
            setBgColor(ContextCompat.getColor(context, bgColor))
        }
    }

    private var onItemClickListener: ((Result, String) -> Unit)? = null

    fun setOnItemClickListener(listener: (Result, String) -> Unit) {
        onItemClickListener = listener
    }

    fun setData(data: List<Result>) {
        val moviesDiffUtil = BaseDiffUtils(moviesList, data)
        val diffUtils = DiffUtil.calculateDiff(moviesDiffUtil)
        moviesList = data
        diffUtils.dispatchUpdatesTo(this)
    }
}

3. 其他关键优化点

  • 移除点击事件中的notifyDataSetChanged():它会强制刷新整个RecyclerView,完全破坏DiffUtils的局部更新逻辑
  • 处理所有null场景:在bind方法中对counseling、startAt等字段为null时,清空对应UI组件,避免旧数据残留
  • 管理网络请求生命周期:在onViewRecycled中取消未完成的头像请求,防止图片错位和内存泄漏
  • 开启getItemId():配合DiffUtils让RecyclerView更精准识别条目,减少复用错误

内容的提问来源于stack exchange,提问作者Jojo Drink

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 00:20:42