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
相关产品推荐
相关产品推荐

