Room结合LiveData与DiffUtil时同内存引用导致列表异常更新问题求助
Room + DiffUtil 列表集成异常解决方案
我正在尝试将列表与Room数据库、DiffUtil进行集成,流程示意图如下:
遇到的问题
- 条目更新时LiveData被触发,但适配器中的现有条目与新返回的newItems完全一致,未给适配器设置新列表就发生了内容变更。示例:OldList = [0 : "zero", 1: "one"],NewList = [0: "0", 1:"1"],更新触发时Adapter.currentList直接变成了[0:"0", 1:"1"]
- 未主动赋值的情况下旧列表与新列表内容完全一致
相关代码
Room - 变更监听
@Query("SELECT * FROM tbl_topic_list ORDER BY created_at DESC") abstract fun getTopicListLiveData():LiveData<List<FilteringTopicEntity>>
LiveData - 变更监听
viewModel.getTopicListLiveData().observe(this, { list: List<FilteringTopicEntity> -> itemAdapter?.let { it.setData(list.toMutableList()) }.orElse { itemAdapter = TopicListingAdapter(list .toMutableList(), itemListener) itemTopicRecyclerView.adapter = itemAdapter } Log.e(TAG, "Item Received ${list.size}") })
数据库更新逻辑
@Update(onConflict = OnConflictStrategy.REPLACE) abstract fun updateFilterTopic(copyDataEntity: FilteringTopicEntity)
override fun updateTopic(topicEntity: FilteringTopicEntity) { topicEntity.updatedAt = DateTime() topicListDao.updateFilterTopic(topicEntity) }
DiffUtil回调实现
class MediaDiffCallback( private val oldList: MutableList<FilteringTopicEntity>, private val newList: MutableList<FilteringTopicEntity> ) : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size override fun getNewListSize(): Int = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].id == newList[newItemPosition].id } override fun areContentsTheSame(oldPosition: Int, newPosition: Int): Boolean { return oldList[oldPosition].updatedAt == newList[newPosition].updatedAt } @Nullable override fun getChangePayload(oldPosition: Int, newPosition: Int): Any? { return super.getChangePayload(oldPosition, newPosition) } }
已尝试的解决方案(均未生效)
- 使用
toMutableList()与toList()转换列表 - 使用data class并通过
objectList.map {it.copy}.toList()转换 - 提取字段值新建对象实例再组装新列表
- 升级为最新版本的livedata、room依赖
未主动调用set方法的情况下,LiveData返回的条目已经出现在适配器中
修复方案
该问题核心是新旧列表存在内存引用冲突,Adapter内部的列表和DiffUtil计算用的列表指向同一块内存地址,导致DiffUtil还未完成计算,原有列表就已经被新数据覆盖,按以下步骤修改即可解决:
- 修正Adapter的setData逻辑:计算Diff前先对旧列表做深拷贝,避免引用冲突,Diff计算完成后再替换内部列表
// 适配器内部setData方法修改示例 fun setData(newList: List<FilteringTopicEntity>) { val oldCopy = currentList.map { it.copy() } val diffResult = DiffUtil.calculateDiff(MediaDiffCallback(oldCopy, newList)) currentList = newList.toMutableList() diffResult.dispatchUpdatesTo(this) }
- 完善DiffUtil内容比对规则:所有和UI展示相关的字段都要参与比对,避免仅比对updatedAt导致的更新遗漏
override fun areContentsTheSame(oldPosition: Int, newPosition: Int): Boolean { val oldItem = oldList[oldPosition] val newItem = newList[newPosition] return oldItem.updatedAt == newItem.updatedAt && oldItem.title == newItem.title && oldItem.content == newItem.content }
- 规范实体类与适配器初始化逻辑:将FilteringTopicEntity的所有字段声明为
val保证不可变;初始化页面时就创建适配器并绑定到RecyclerView,不要在LiveData回调中重复创建适配器
// 页面初始化阶段执行 itemAdapter = TopicListingAdapter(mutableListOf(), itemListener) itemTopicRecyclerView.adapter = itemAdapter // LiveData回调仅更新数据 viewModel.getTopicListLiveData().observe(this, { list -> itemAdapter?.setData(list) })
修改后DiffUtil会正常计算新旧列表差异,自动触发对应条目更新,无需调用notifyDataSetChanged()。
内容的提问来源于stack exchange,提问作者Merlin Jeyakumar
相关产品推荐
相关产品推荐

