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

Kotlin RecyclerView结合DiffUtil与ViewModel更新列表状态求助

修复方案

你现有代码不符合MVVM数据驱动原则的核心问题有4个,按下面步骤修改即可正常运行:

  • 直接修改MutableLiveData内部持有列表的元素属性,没有主动更新LiveData值,观察者不会收到变更通知
  • DiffUtil回调里索引用错,对比新列表项时误用了旧列表的位置参数,会导致增量更新判断错误
  • Adapter点击回调里手动修改点赞按钮状态,属于UI层直接持有状态,后续列表刷新时容易出现状态错位
  • Employee数据类的isLiked设为可变变量,容易被非ViewModel层随意修改,导致状态不可控

1. 修正Employee数据类

把isLiked改为不可变变量,所有状态变更通过data class自带的copy()方法生成新对象实现,避免外部随意修改状态:

data class Employee(
    val id: Long,
    val fullName: String,
    val city: String,
    val isLiked: Boolean = false // 改为val,禁止直接修改属性
) {
    companion object {
        fun getMockEmployees() = listOf(
            Employee(0, "John Johnson", "London"),
            Employee(1, "John Johnson", "London"),
            Employee(2, "John Johnson", "London"),
            Employee(3, "John Johnson", "London")
        )
    }
}

2. 修复DiffUtil回调的索引错误

对比新旧列表项时,新列表必须使用newItemPosition作为索引,同时对比所有会影响UI展示的字段,避免漏刷:

class EmployeesDiffUtilCallback(
    private val oldList: List<Employee>,
    private val newList: List<Employee>
) : DiffUtil.Callback() {
    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return oldItem.id == newItem.id
                && oldItem.fullName == newItem.fullName
                && oldItem.city == newItem.city
                && oldItem.isLiked == newItem.isLiked
    }
}

3. 调整ViewModel的点赞逻辑

不要直接修改原列表内的对象,而是生成全新的列表,翻转对应项的点赞状态后重新赋值给LiveData,才能触发UI层的观察者回调:

class MainActivityViewModel : ViewModel() {
    val employees = MutableLiveData(Employee.getMockEmployees())

    fun likeEmployee(position: Int) {
        val currentList = employees.value ?: return
        val newList = currentList.toMutableList()
        val target = newList[position]
        // 复制生成状态翻转后的新对象,不修改原对象
        newList[position] = target.copy(isLiked = !target.isLiked)
        // 重新赋值,触发LiveData回调
        employees.value = newList
    }
}

4. 修正Adapter逻辑

移除点击事件里手动修改点赞按钮可见性的代码,所有UI状态完全由onBindViewHolder拿到的数据源决定;同时把点击事件绑定移到ViewHolder初始化块里,避免每次绑定都重复创建监听器,用bindingAdapterPosition获取真实位置避免列表错位:

class EmployeeAdapter(
    private val clickedLike: (Int) -> Unit
) : RecyclerView.Adapter<EmployeeAdapter.EmployeeViewHolder>() {

    private val employees = mutableListOf<Employee>()

    inner class EmployeeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val employeeCard: ConstraintLayout = itemView.findViewById(R.id.employees_list_item)
        val fullNameTextView: TextView = itemView.findViewById(R.id.full_name)
        val likeButton: ImageView = itemView.findViewById(R.id.like_button)

        init {
            employeeCard.setOnClickListener {
                val realPosition = bindingAdapterPosition
                if (realPosition != RecyclerView.NO_POSITION) {
                    clickedLike(realPosition)
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmployeeViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.employees_list_item, parent, false)
        return EmployeeViewHolder(view)
    }

    override fun onBindViewHolder(holder: EmployeeViewHolder, position: Int) {
        val employee = employees[position]
        with(holder) {
            fullNameTextView.text = employee.fullName
            likeButton.visibility = if (employee.isLiked) View.VISIBLE else View.INVISIBLE
        }
    }

    override fun getItemCount(): Int = employees.size

    fun reload(data: List<Employee>) {
        val diffResult = DiffUtil.calculateDiff(EmployeesDiffUtilCallback(employees, data))
        employees.clear()
        employees.addAll(data)
        diffResult.dispatchUpdatesTo(this)
    }
}

MainActivity层的现有代码不需要修改,它已经正确观察了ViewModel的employees数据,只要ViewModel推送新列表,就会自动调用Adapter的reload方法走DiffUtil增量更新。

小提示:如果后续列表操作场景更多,可以直接用官方封装好的ListAdapter,它内部已经实现了DiffUtil逻辑和列表维护,不需要自己写reload方法和持有列表数据,代码会更简洁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 09:03:21