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

