带筛选与Payload更新的RecyclerView Adapter同步问题咨询
基于Filter和AsyncListDiffer的RecyclerView Adapter状态同步问题
问题背景
需要实现支持Filter筛选、Payload局部更新的RecyclerView Adapter,因数据量较大,采用AsyncListDiffer做异步更新。
核心问题
- 筛选状态下修改列表项的收藏状态(星号标记),清除筛选后该状态丢失
- AsyncListDiffer异步更新时存在元素更新冲突
怀疑点
问题出在submitList和updateItem方法的逻辑中,原代码如下:
class Adapter( private val listener: AdapterListener ): RecyclerView.Adapter<RecyclerView.ViewHolder>(), Filterable { private val diffCallback = object : DiffUtil.ItemCallback<UiModel>() { override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { return ( oldItem.name == newItem.name && oldItem.id == newItem.id && oldItem.isFavorite == newItem.isFavorite ) } } private var listDiffer = AsyncListDiffer(this, diffCallback) private val initialDataItems = mutableListOf<UiModel>() private val dataFilter = object : Filter() { override fun performFiltering(constraint: CharSequence?): FilterResults { val queryRawName = constraint?.toString() if (queryRawName.isNullOrEmpty()) return FilterResults().apply { values = initialDataItems } val queryName = queryRawName.lowercase().trim() val filteredItems = initialDataItems.filter { it.name?.lowercase()?.contains(queryName).isTrue() } return FilterResults().apply { values = filteredItems } } override fun publishResults(constraint: CharSequence?, results: FilterResults?) { val values = results?.values as? List<*> ?: return val filteredValues = values.filterIsInstance(UiModel::class.java) if (values.size != filteredValues.size) return listDiffer.submitList(filteredValues) } } override fun getItemCount() = listDiffer.currentList.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view = parent.inflate(R.layout.item_layout) return CustomViewHolder( view = view, listener = { listener.onClickFavorite(uiModel) } ) } override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any> ) { val payloadValid = payloads.isNotEmpty() && payloads[0] is UiModel if (payloadValid && holder is CustomViewHolder) { holder.updatePayload(payloads[0] as UpdateUiModel) } else { super.onBindViewHolder(holder, position, payloads) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = getItem(position) (holder as? CustomViewHolder)?.bind(item) } override fun getFilter() = dataFilter fun submitList(data: List<UiModel>, updateInitialItems: Boolean = true) { listDiffer.submitList(data) if (updateInitialItems) { initialDataItems.clear() initialDataItems.addAll(data) } } fun updateItem(payload: UpdateUiModel) { if (payload.id == null) return listDiffer.currentList.forEachIndexed { index, dataItem -> if (dataItem.id == payload.id) { updateModel(index, payload) notifyItemChanged(index, payload) } } } private fun isCorrectPosition(position: Int) = position in 0 until itemCount private fun updateModel(index: Int, payload: UiModel) { val currentList = listDiffer.currentList.toMutableList() if (isCorrectPosition(index)) currentList[index] = currentList[index].updateModel(payload) submitList(currentList) } private fun getItem(position: Int) = listDiffer.currentList[position] }
问题根源分析
- 原始数据源与显示列表不同步:修改收藏状态时只更新了当前筛选后的列表,
initialDataItems(原始数据源)未同步更新,导致清除筛选后恢复原始数据,丢失修改状态。 - 更新逻辑冲突:
updateModel中调用submitList触发全量Diff,同时又调用notifyItemChanged做局部更新,异步执行时会导致状态覆盖。 - Payload类型判断错误:
onBindViewHolder中判断Payload是UiModel,但实际传入的是UpdateUiModel,导致局部更新逻辑未触发。
修复方案
1. 同步更新原始数据源和显示列表
修改updateItem方法,同时更新原始数据和当前显示列表,避免状态丢失:
fun updateItem(payload: UpdateUiModel) { payload.id ?: return // 先更新原始数据源 initialDataItems.forEachIndexed { idx, item -> if (item.id == payload.id) { initialDataItems[idx] = item.updateModel(payload) } } // 再更新当前显示列表并触发局部更新 val currentList = listDiffer.currentList.toMutableList() currentList.forEachIndexed { idx, item -> if (item.id == payload.id) { currentList[idx] = item.updateModel(payload) notifyItemChanged(idx, payload) } } }
2. 移除冲突的全量更新逻辑
删除updateModel方法,避免全量Diff和局部更新的冲突:
// 移除该方法 // private fun updateModel(index: Int, payload: UiModel) { // val currentList = listDiffer.currentList.toMutableList() // if (isCorrectPosition(index)) currentList[index] = currentList[index].updateModel(payload) // submitList(currentList) // }
3. 修正Payload类型判断
在onBindViewHolder中正确识别UpdateUiModel类型:
override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any> ) { val payloadValid = payloads.isNotEmpty() && payloads[0] is UpdateUiModel if (payloadValid && holder is CustomViewHolder) { holder.updatePayload(payloads[0] as UpdateUiModel) return // 避免调用super执行全量绑定 } super.onBindViewHolder(holder, position, payloads) }
4. 优化Filter结果传递
确保筛选结果是新的列表实例,让AsyncListDiffer正确识别变化:
override fun publishResults(constraint: CharSequence?, results: FilterResults?) { val filteredValues = (results?.values as? List<UiModel>) ?: emptyList() listDiffer.submitList(filteredValues.toList()) // 传递新列表实例 }
5. 确保数据类不可变
UiModel设计为不可变数据类,updateModel返回新实例,避免引用传递导致的状态混乱:
data class UiModel( val id: String, val name: String?, val isFavorite: Boolean ) { fun updateModel(payload: UpdateUiModel): UiModel { return copy(isFavorite = payload.isFavorite ?: this.isFavorite) } } data class UpdateUiModel( val id: String?, val isFavorite: Boolean? )
完整修复后的Adapter代码
class Adapter( private val listener: AdapterListener ): RecyclerView.Adapter<RecyclerView.ViewHolder>(), Filterable { private val diffCallback = object : DiffUtil.ItemCallback<UiModel>() { override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean { return oldItem.name == newItem.name && oldItem.id == newItem.id && oldItem.isFavorite == newItem.isFavorite } } private var listDiffer = AsyncListDiffer(this, diffCallback) private val initialDataItems = mutableListOf<UiModel>() private val dataFilter = object : Filter() { override fun performFiltering(constraint: CharSequence?): FilterResults { val queryRawName = constraint?.toString() return if (queryRawName.isNullOrEmpty()) { FilterResults().apply { values = initialDataItems.toList() } } else { val queryName = queryRawName.lowercase().trim() val filteredItems = initialDataItems.filter { it.name?.lowercase()?.contains(queryName) == true } FilterResults().apply { values = filteredItems } } } override fun publishResults(constraint: CharSequence?, results: FilterResults?) { val filteredValues = (results?.values as? List<UiModel>) ?: emptyList() listDiffer.submitList(filteredValues.toList()) } } override fun getItemCount() = listDiffer.currentList.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view = parent.inflate(R.layout.item_layout) return CustomViewHolder( view = view, listener = { listener.onClickFavorite(it) } ) } override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any> ) { val payloadValid = payloads.isNotEmpty() && payloads[0] is UpdateUiModel if (payloadValid && holder is CustomViewHolder) { holder.updatePayload(payloads[0] as UpdateUiModel) return } super.onBindViewHolder(holder, position, payloads) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = getItem(position) (holder as? CustomViewHolder)?.bind(item) } override fun getFilter() = dataFilter fun submitList(data: List<UiModel>, updateInitialItems: Boolean = true) { listDiffer.submitList(data.toList()) if (updateInitialItems) { initialDataItems.clear() initialDataItems.addAll(data) } } fun updateItem(payload: UpdateUiModel) { payload.id ?: return // 更新原始数据源 initialDataItems.forEachIndexed { idx, item -> if (item.id == payload.id) { initialDataItems[idx] = item.updateModel(payload) } } // 更新当前显示列表并触发局部更新 val currentList = listDiffer.currentList.toMutableList() currentList.forEachIndexed { idx, item -> if (item.id == payload.id) { currentList[idx] = item.updateModel(payload) notifyItemChanged(idx, payload) } } } private fun getItem(position: Int) = listDiffer.currentList[position] interface AdapterListener { fun onClickFavorite(uiModel: UiModel) } } // 配套数据类示例 data class UiModel( val id: String, val name: String?, val isFavorite: Boolean ) { fun updateModel(payload: UpdateUiModel): UiModel { return copy(isFavorite = payload.isFavorite ?: this.isFavorite) } } data class UpdateUiModel( val id: String?, val isFavorite: Boolean? )
内容的提问来源于stack exchange,提问作者Tim Meleshko
相关产品推荐
相关产品推荐

