如何更新ListAdapter时避免notifyDataSetChange式全量刷新且不回滚到顶部?
ListAdapter submitList全量刷新、滚动回顶部的解决方案
问题根因
ListAdapter本身设计就是通过DiffUtil做增量刷新,不会默认触发全列表刷新,出现notifyDataSetChanged级别的全量刷新、列表自动滚回顶部,本质是DiffUtil差异比对失效,或者列表配置错误:
- 列表项实体类没有正确实现相等性判断,DiffUtil默认用引用相等判定,内容相同的新对象会被判定为全新项
- 提交列表时传入了和当前内部持有列表同一个引用的可变列表,DiffUtil直接跳过差异计算
- 重复设置RecyclerView的Adapter或LayoutManager,强制重置了列表状态
- RecyclerView高度设置为
wrap_content,每次数据变化会触发整个布局重新测量,重置滚动位置
落地解决步骤
- 首先修正列表项实体的相等性逻辑
Kotlin环境直接将列表项实体声明为data class,编译器会自动基于主构造属性生成正确的equals()和hashCode()实现;Java环境手动重写两个方法,保证内容一致的对象判定为相等,嵌套的自定义类属性也要同步实现相等性判断,不要使用默认的对象引用相等逻辑。 - 规范submitList的传参
不要直接修改Adapter内部已经持有的原列表后传入同一个引用,正确做法是创建全新的列表副本承载新数据再传入:// Kotlin示例:拷贝生成新列表 val updateList = oldList.map { it.copy(isSelected = false) } // 按需修改数据 adapter.submitList(updateList.toList()) // 转成不可变新实例传入// Java示例:拷贝生成新列表 List<Item> updateList = new ArrayList<>(); for (Item item : oldList) { updateList.add(new Item(item.getId(), item.getContent())); // 构造新数据项 } adapter.submitList(updateList); - 避免重复初始化RecyclerView配置
setAdapter()、setLayoutManager()仅在页面/视图初始化阶段调用一次即可,不要在每次数据刷新逻辑里重复调用,否则会直接重置列表的滚动状态和缓存。 - 修正RecyclerView布局参数
给RecyclerView设置固定高度或者match_parent,不要用wrap_content,避免每次数据变化触发全局重新测量导致位置重置;如果必须用wrap_content,给RecyclerView加上setHasFixedSize(true)优化测量逻辑。 - 兜底滚动位置恢复
如果以上配置都调整完仍有轻微位置偏移,可以在提交列表前记录当前滚动位置,等差异计算完成后恢复位置:val lm = recyclerView.layoutManager as LinearLayoutManager val firstPos = lm.findFirstVisibleItemPosition() val topOffset = lm.findViewByPosition(firstPos)?.top ?: 0 // submitList的第二个参数是提交完成的回调,在回调里恢复位置 adapter.submitList(newItems) { lm.scrollToPositionWithOffset(firstPos, topOffset) }
注意:不要随意重写ListAdapter的
getItemCount()、getItem()方法,默认实现已经和内部存储的列表做了绑定,自定义返回逻辑会导致DiffUtil位置计算错位,触发异常全量刷新。
内容的提问来源于stack exchange,提问作者Kamran Akbary
相关产品推荐
相关产品推荐

