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

Room Database中@Delete注解删除列表选中任务失效问题

问题根因

删除已完成任务不生效是3个核心问题叠加导致的:

  • DAO层的readAllData()方法漏了@Query注解,Room无法识别这个查询方法,数据库读写本身存在基础问题
  • 复选框点击后只修改了内存里的临时Todo对象,没有把勾选状态同步更新到数据库,删除SQL读的是数据库存储的字段值,永远拿不到UI上修改的勾选状态,自然匹配不到要删除的数据
  • 筛选已完成任务用了takeWhile,这个方法只会返回列表开头连续满足条件的元素,只要中间夹了一个未完成任务,后面的已完成任务会全部漏掉;另外在Adapter里直接new新的ListFragment实例调用ViewModel,拿到的是完全没绑定生命周期的无效实例,操作不会生效。
分步修复方案

1. 修复DAO层基础注解缺失

给readAllData补上正确的查询注解,新增更新方法用来同步复选框状态,修正后的DAO代码:

@Dao
interface TodoDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun addTodo(todo: Todo)

    // 补上之前遗漏的@Query注解
    @Query("SELECT * FROM todo_table ORDER BY id ASC")
    fun readAllData() : LiveData<List<Todo>>

    @Query("DELETE FROM todo_table WHERE todoCheckBox = 1")
    suspend fun deleteSelectedTasks()

    @Query("DELETE FROM todo_table")
    suspend fun deleteAllTasks()

    // 新增更新方法,同步复选框状态到数据库
    @Update
    suspend fun updateTodo(todo: Todo)
}

*同步在TodoViewModel中添加对应的更新方法,和其他数据库操作保持一致的协程作用域:

fun updateTodo(todo: Todo) {
    viewModelScope.launch {
        todoDao.updateTodo(todo)
    }
}

2. 修复Adapter里的复选框状态同步逻辑

不要在Adapter里直接new Fragment操作ViewModel,改成通过回调把状态变更传给外层Fragment,同时修正勾选状态赋值反了的逻辑错误:

class ListAdapter(
    // 新增回调,把勾选变更事件传给外层Fragment
    private val onTodoCheckChanged: (Todo) -> Unit
): Adapter<ListAdapter.TodoViewHolder>() {
    private var todoList = emptyList<Todo>()

    // 给标题加/删删除线
    private fun toggleStrikeThrough(tvTaskTitle : TextView, cbTask : Boolean) {
        if (cbTask) {
            tvTaskTitle.paintFlags = tvTaskTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
        } else {
            tvTaskTitle.paintFlags = tvTaskTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
        }
    }

    inner class TodoViewHolder(val binding : CustomRowBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
        val binding = CustomRowBinding.inflate(LayoutInflater.from(parent.context),
            parent,
            false
        )
        return TodoViewHolder(binding)
    }

    override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
        val todo = todoList[position]
        val dateLocales = SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.getDefault())
        val timeLocales = SimpleDateFormat(SIMPLE_TIME_FORMAT, Locale.getDefault())
        holder.apply {
            binding.tvTaskTitle.text = todo.title
            binding.tvTaskDate.text = dateLocales.format(todo.date)
            binding.tvTaskTime.text = timeLocales.format(todo.time)
            binding.cbTask.isChecked = todo.todoCheckBox

            toggleStrikeThrough(binding.tvTaskTitle , todo.todoCheckBox)
            binding.cbTask.setOnCheckedChangeListener(null) // 先清空旧监听器,避免列表复用导致勾选错位
            binding.cbTask.setOnCheckedChangeListener { _, isChecked ->
                toggleStrikeThrough(binding.tvTaskTitle, isChecked)
                val updatedTodo = todo.copy(todoCheckBox = isChecked)
                // 把更新后的对象通过回调传出去
                onTodoCheckChanged(updatedTodo)
            }
        }
    }

    override fun getItemCount() = todoList.size

    @SuppressLint("NotifyDataSetChanged")
    fun setData(todo : List<Todo>) {
        this.todoList = todo
        notifyDataSetChanged()
    }
}

3. 修复ListFragment里的逻辑

初始化Adapter的时候传入回调处理状态更新,删掉错误的内存筛选逻辑——DAO层的删除SQL本身就会筛选所有todoCheckBox=1的条目,不需要提前在内存里过滤:

class ListFragment : Fragment() {
    private var _binding : FragmentListBinding? = null
    private val binding get() = _binding!!
    lateinit var mTodoViewModel: TodoViewModel
    private lateinit var recyclerView: RecyclerView
    // 初始化Adapter时传入复选框点击回调
    private val adapter = ListAdapter { updatedTodo ->
        // 收到回调就调用ViewModel的更新方法,把状态同步到数据库
        lifecycleScope.launch {
            mTodoViewModel.updateTodo(updatedTodo)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentListBinding.inflate(inflater, container, false)
        setHasOptionsMenu(true)

        recyclerView = binding.recyclerViewTodo
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(requireContext())

        mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
        mTodoViewModel.readAllData.observe(viewLifecycleOwner) { todos ->
            adapter.setData(todos)
        }

        binding.fbAdd.setOnClickListener {
            findNavController().navigate(R.id.action_listFragment_to_addFragment)
        }

        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.fragment_list, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when(item.itemId)  {
            R.id.del_selected_tasks -> {
                deleteSelectedTasks()
                true
            }
            R.id.del_all_tasks -> {
                deleteAllTasks()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    private fun deleteAllTasks() {
        val builder = AlertDialog.Builder(requireContext())
        builder.setPositiveButton("Yes") {_,_->
            mTodoViewModel.deleteAllTasks()
            Toast.makeText(requireContext(), "All tasks have been successfully deleted!", Toast.LENGTH_LONG).show()
        }
        builder.setNegativeButton("No") {_,_-> }
        builder.setTitle("Confirm Deletion")
        builder.setMessage("Are you sure you want to delete all Tasks?")
        builder.create().show()
    }

    private fun deleteSelectedTasks() {
        val builder = AlertDialog.Builder(requireContext())
        builder.setPositiveButton("Yes") {_,_->
            // 直接调用删除方法即可,SQL会自动匹配所有已勾选的任务
            mTodoViewModel.deleteSelectedTasks()
            Toast.makeText(requireContext(), "Selected tasks deleted!", Toast.LENGTH_LONG).show()
        }
        builder.setNegativeButton("No") {_,_->}
        builder.setTitle("Confirm Deletion")
        builder.setMessage("Are you sure you want to delete only selected Tasks?")
        builder.create().show()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}
额外注意点
  • RecyclerView的Adapter只负责渲染UI,所有数据操作都要通过回调传给持有生命周期的Fragment/Activity处理,不要在Adapter里直接new Fragment或者拿ViewModel实例,不然会出现内存泄漏、操作无效的问题
  • 如果确实需要在内存里筛选所有已完成任务,用filter { it.todoCheckBox },不要用takeWhile,后者遇到第一个不满足条件的元素就会停止遍历,很容易漏选数据

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 10:27:48