Android MVVM架构下RecyclerView更新数据时滚动位置跳转问题
这个问题我之前在项目里也踩过坑!核心原因就是你每次更新**整个List<ListItem>**的时候,RecyclerView会判定整个数据集发生了全局变化,触发全量刷新,最终导致滚动位置意外跳转。下面给你几个实用的解决方案:
1. 使用 DiffUtil 实现局部刷新
DiffUtil可以帮RecyclerView精准识别出哪些数据项发生了变化,只刷新对应的Item,而不是整个列表。
步骤:
- 首先创建一个
DiffUtil.Callback的实现类,对比新旧列表的差异:
class ListItemDiffCallback( private val oldList: List<ListItem>, private val newList: List<ListItem> ) : DiffUtil.Callback() { override fun getOldListSize() = oldList.size override fun getNewListSize() = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { // 用唯一标识判断是不是同一个Item,比如id return oldList[oldItemPosition].id == newList[newItemPosition].id } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { // 判断Item的内容是否一致,这里要包含isselected状态 val oldItem = oldList[oldItemPosition] val newItem = newList[newItemPosition] return oldItem.isSelected == newItem.isSelected && oldItem.content == newItem.content } }
- 然后在Adapter里更新数据的时候,用DiffUtil计算差异并刷新:
fun updateData(newList: List<ListItem>) { val diffResult = DiffUtil.calculateDiff(ListItemDiffCallback(currentList, newList)) currentList = newList diffResult.dispatchUpdatesTo(this) }
- ViewModel里更新LiveData的时候,依然可以传递新列表,但Adapter只会刷新状态变化的那个Item,不会影响滚动位置。
2. 直接更新单个Item的状态,而非整个列表
如果你的ListItem是可观察的(比如用ObservableField或者Data Binding的双向绑定),可以直接修改选中项的isSelected状态,让Data Binding自动刷新该Item,不需要更新整个LiveData列表。
示例:
- 先让ListItem类的
isSelected变成可观察的:
data class ListItem( val id: String, val content: String, val isSelected: ObservableBoolean = ObservableBoolean(false) )
- ViewModel里处理选中事件时,直接修改对应Item的状态:
fun onItemSelected(item: ListItem) { // 切换选中状态 item.isSelected.set(!item.isSelected.get()) // 不需要更新整个LiveData<List<ListItem>> }
- 在Item的布局里用Data Binding绑定状态:
<TextView android:id="@+id/tv_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{item.content}" android:background="@{item.isSelected ? @color/selected_color : @color/white}" />
这种方式更高效,完全不会触发列表的全局刷新,滚动位置自然不会跳。
3. 避免创建新的List实例,修改原有List的状态后通知更新
如果你不想用DiffUtil或者Data Binding,也可以在ViewModel里修改原有List中对应Item的isSelected状态,然后调用liveData.value = currentList(注意要保证List是同一个实例,但内部元素状态变化),不过这时候需要Adapter支持局部刷新,比如调用notifyItemChanged(position)。
注意:
这种方式需要你能获取到选中Item在列表中的位置,ViewModel里可能需要额外维护位置信息,相对前两种方式稍麻烦,但也是可行的。
总结
最推荐的是第一种+第二种结合:用DiffHandle处理列表的整体更新(比如加载更多、刷新数据),用可观察的Item状态处理单个选中事件,这样既保证性能,又能完美解决滚动跳转的问题。
内容的提问来源于stack exchange,提问作者itarill

