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
相关产品推荐
相关产品推荐

