You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

更新数据导致RecyclerView(List Adapter)将数据移至列表末尾

问题描述

我正在使用List Adapter展示任务列表:
初始列表展示

每当我更新RecyclerView中的数据时,数据会滚动至列表末尾:
更新后滚动至末尾

同时发现,更新数据库中的数据时,Room会自动将该数据移至最后索引位置:
更新前:
更新前的列表
更新后:
更新后的列表

请问这是什么原因导致的?


参考代码

适配器代码

class TestAdapter(private val taskListener: DetailTaskAdapterListener) : ListAdapter<Task, TestAdapter.TestViewHolder>(DiffUtilCallback) {

    inner class TestViewHolder(private val binding: DetailSubTaskItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(task: Task) {
            binding.checkbox.isChecked = task.isComplete
            binding.task.setText(task.task.trim())
        }

        fun setupListener() {
            binding.checkbox.setOnCheckedChangeListener { _, isChecked ->
                if (isChecked) {
                    binding.task.paintFlags = Paint.STRIKE_THRU_TEXT_FLAG
                } else {
                    binding.task.paintFlags = 0
                }
                getItem(absoluteAdapterPosition).copy(isComplete = isChecked)
            }

            binding.task.setOnFocusChangeListener { view, hasFocus ->
                if(!hasFocus){
                    if(binding.task.text.toString().trim().isEmpty()){
                        taskListener.deleteTask(getItem(absoluteAdapterPosition).taskId)
                    }else{
                        taskListener.updateTask(getItem(absoluteAdapterPosition).taskId, binding.task.text.toString().trim())
                    }
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return TestViewHolder(
            DetailSubTaskItemBinding.inflate(
                layoutInflater, parent, false
            )
        )
    }

    override fun onBindViewHolder(holder: TestViewHolder, position: Int) {
        val task = getItem(position)
        holder.setupListener()
        holder.bind(task)
    }

    companion object DiffUtilCallback : DiffUtil.ItemCallback<Task>() {
        override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem.task == newItem.task
        }

        override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem == newItem
        }
    }
}

DAO代码

(注:插入和更新操作都会导致RecyclerView将更新数据推至最后一行)

@Transaction
@Query("SELECT * FROM todo WHERE todoId = :todoId")
fun getTodoDetails(todoId: String): Flow<TodoDetailsEntity>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity): Long

@Query("UPDATE task SET task = :title WHERE taskId = :taskId")
suspend fun updateTaskTitle(taskId: String, title: String): Int

实体模型代码

data class TodoDetailsEntity(
    @Embedded val todo: TodoEntity,
    @Relation(
        parentColumn = "todoId",
        entityColumn = "todoRefId"
    )
    val tasks: List<TaskEntity>,
    @Relation(
        parentColumn = "todoId",
        entityColumn = "noteId"
    )
    val note: NoteEntity?,
    @Relation(
        parentColumn = "todoId",
        entityColumn = "todoRefId"
    )
    val attachments: List<AttachmentEntity>
)

Activity中submitList调用代码

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.saveTodoId(intent.extras?.getString("todoId")!!)
        todoId = intent.extras?.getString("todoId")
        binding = ActivityDetailsBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setupToolbar()
        setupTestAdapter()
        collectLatestLifecycleFlow(viewModel.uiState) { uiState ->

            testAdapter.submitList(uiState.todoDetails?.tasks)
        }
    }

原因分析与解决方案

核心原因1:DiffUtil的条目判断逻辑错误

当前用oldItem.task == newItem.task判断是否为同一个条目,但任务内容是可修改的——一旦任务名称改变,DiffUtil会认为这是新条目,而非原条目更新。此时ListAdapter会移除原条目、将新条目添加到列表末尾,直接触发滚动到末尾的现象。

修复方式:改用唯一标识(比如taskId)判断条目是否相同:

companion object DiffUtilCallback : DiffUtil.ItemCallback<Task>() {
    override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
        // 用唯一ID判断,而非可修改的任务内容
        return oldItem.taskId == newItem.taskId
    }

    override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
        return oldItem == newItem
    }
}

核心原因2:Room查询未指定固定排序规则

Room默认按数据插入顺序返回列表,执行REPLACE或UPDATE操作时,底层存储顺序可能变动,导致查询返回的任务列表顺序改变,进而触发ListAdapter提交新列表时出现位置偏移。

修复方式:给任务查询指定固定排序规则,比如按taskId或创建时间排序:

  • 方式1:在TaskEntity的@Entity注解中指定默认排序:
@Entity(tableName = "task", orderBy = "taskId ASC")
data class TaskEntity(
    val taskId: String,
    val todoRefId: String,
    val task: String,
    val isComplete: Boolean
    // 其他字段...
)
  • 方式2:在@Relation注解中直接指定排序:
@Relation(
    parentColumn = "todoId",
    entityColumn = "todoRefId",
    sortColumn = "taskId ASC"
)
val tasks: List<TaskEntity>,

额外注意点:submitList的调用细节

collectLatestLifecycleFlow会在uiState变化时提交列表,确保提交的是新的列表实例(避免复用旧列表对象),否则DiffUtil可能无法正确计算差异。如果uiState.todoDetails?.tasks是同一个实例,建议提交前拷贝:

testAdapter.submitList(uiState.todoDetails?.tasks?.toList())

内容的提问来源于stack exchange,提问作者JV17

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 22:00:59