Search code examples
androidmysqlkotlinannotationsandroid-room

@Delete annotation in Room Database not working for lists


I have a TodoList App and I am trying to delete a list Completed tasks based on a strikeThrough over the Task. I have set several Implementations to single out the Task that has a strikeThrough in it which I'm honestly not sure if it works properly but I definitely know something is wrong with the Delete Query as well because it wasn't also working. The second Implementation is attached to the main one but I commented it out.

This is my Model

import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*

/** Our Model class. This class will represent our database table **/


@Entity(tableName = "todo_table")
data class Todo(
    @PrimaryKey (autoGenerate = true) // here "Room" will autoGenerate the id for us 
instead of assigning a randomUUID value
    val id : Int = 0,
    var title : String = "",
    var date : Date = Date(),
    var time : Date = Date(),
    var todoCheckBox : Boolean = false
)

Then My Dao.

 import androidx.lifecycle.LiveData
 import androidx.room.*
 import com.bignerdranch.android.to_dolist.model.Todo

 /**
  *  This will be our DAO file where we will be update, delete and add Todos to our 
 database so it contains the methods used for accessing the database
  */

 @Dao
 interface TodoDao {

    // onConflict will ignore any known conflicts, in this case will remove duplicate 
    "Todos" with the same name
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun addTodo(todo: Todo)

    fun readAllData() : LiveData<List<Todo>>

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

    // second method - This is the second method where I tried passing in an array
    //    @Delete
//    suspend fun deleteSelectedTasks(todoList : Array<Todo>)

    // Method in use
    @Query("DELETE FROM todo_table")
    suspend fun deleteAllTasks()
}

And then this is my ListFragment and ListAdapter. I have tried calling the deleteSelectedTasks through the ViewModel from both the Fragment and Adapter but none as worked.

import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bignerdranch.android.to_dolist.databinding.FragmentListBinding
import com.bignerdranch.android.to_dolist.R
import com.bignerdranch.android.to_dolist.data.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo

private const val TAG = "ListFragment"

class ListFragment : Fragment() {
    private var _binding : FragmentListBinding? = null
    private val binding get() = _binding!!
    lateinit var mTodoViewModel: TodoViewModel
    private lateinit var recyclerView: RecyclerView
    private val adapter = ListAdapter()  // getting reference to our ListAdapter
    private lateinit var selectedTodos : List<Todo>

    // second method
//    var selectedTodos = arrayOf<Todo>()


    // TODO - WHEN I COME BACK, I WILL SEE IF I CAN DO THE IMPLEMENTATION HERE IN THE LIST 
FRAGMENT

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment with ViewBinding style
        _binding = FragmentListBinding.inflate(inflater, container, false)

        // this tells our activity/fragment that we have a menu_item it should respond to.
        setHasOptionsMenu(true)

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



        /**
         *  updates our recyclerView with the new "observed" changes in our database through 
our adapter
         */
        // TodoViewModel
        mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
        mTodoViewModel.readAllData.observe(viewLifecycleOwner) { todos ->
            adapter.setData(todos)
            selectedTodos = todos
        }

        // Add Task Button
        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 -> {
                deleteSelectedUsers()
                true
            }

            R.id.del_all_tasks -> {
                deleteAllTasks()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    // function to delete all of our Tasks
    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()
    }

    // function to delete only selected Tasks
    @SuppressLint("NotifyDataSetChanged")
    private fun deleteSelectedUsers() {
        val builder = AlertDialog.Builder(requireContext())
//        val todo = emptyList<Todo>()
        val finishedTodos = selectedTodos.takeWhile {  it.todoCheckBox }
        builder.setPositiveButton("Yes") {_,_->
        mTodoViewModel.deleteSelectedTasks()
            adapter.notifyDataSetChanged()
        }
        builder.setNegativeButton("No") {_,_->}
        builder.setTitle("Confirm Deletion")
        builder.setMessage("Are you sure you want to delete only selected Tasks?")
        builder.create().show()
        Log.d(TAG, "Our todos $selectedTodos and $finishedTodos")
    }


    // We want to leave no trace of our Binding class Reference to avoid memory leaks
    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}

ListAdapter

import android.annotation.SuppressLint
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
import com.bignerdranch.android.to_dolist.databinding.CustomRowBinding
import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_DATE_FORMAT
import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_TIME_FORMAT
import com.bignerdranch.android.to_dolist.model.Todo
import java.text.SimpleDateFormat
import java.util.*

private const val TAG = "ListAdapter"

class ListAdapter: Adapter<ListAdapter.TodoViewHolder>() {
    private var todoList = emptyList<Todo>()
    private var todo = Todo()


    // will toggle strikeThrough on the Task title
    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 {
        // this can be done in an inline variable and I may experiment on it later.
        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 { _, isChecked ->
                toggleStrikeThrough(binding.tvTaskTitle, isChecked)
                todo.todoCheckBox = !todo.todoCheckBox

                taskCheck(todoList as MutableList<Todo>)
            }
        }
    }

    private fun taskCheck(todo : List<Todo>) {
        val listFragment = ListFragment()
        val finishedTodos = todo.takeWhile {  it.todoCheckBox }
        // second method
//        listFragment.selectedTodos = finishedTodos.toTypedArray()

        Log.i(TAG, "Our ${finishedTodos.size}")
    }


    // as usual will return the size of the List
    override fun getItemCount() = todoList.size

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

    @SuppressLint("NotifyDataSetChanged")
    fun deleteSelectedTasks() {
        val listFragment = ListFragment()
        listFragment.mTodoViewModel.deleteSelectedTasks()
        notifyDataSetChanged()
    }
}

Solution

  • @Delete does work with lists.

    Consider the following demo based upon your code (with changes made for the convenience and brevity of running on the main thread and the avoidance of handling date conversion)

    • i.e. suspend removed, columns with type Date changed to String

    So Todo is :-

    @Entity(tableName = "todo_table")
    data class Todo(
        @PrimaryKey(autoGenerate = true) // here "Room" will autoGenerate the id for us instead of assigning a randomUUID value
        val id : Int = 0,
        var title : String = "",
        var date : String /* Date = Date()*/,
        var time : String /* Date = Date()*/,
        var todoCheckBox : Boolean = false
    )
    

    and TodoDao is :-

    @Dao
    interface TodoDao {
    
        // onConflict will ignore any known conflicts, in this case will remove duplicate "Todos" with the same name
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        /*suspend*/ fun addTodo(todo: Todo)
    
        @Query("SELECT * FROM todo_table")
        fun readAllData() : /*LiveData<*/List<Todo>/*>*/
    
        @Query("DELETE FROM todo_table WHERE todoCheckBox = 1")
        /*suspend*/ fun deleteSelectedTasks()
    
        // second method - This is the second method where I tried passing in an array
        @Delete
        /*suspend*/ fun deleteSelectedTasks(todoList : Array<Todo>)
    
        // Method in use
        @Query("DELETE FROM todo_table")
        /*suspend*/ fun deleteAllTasks()
    }
    

    The using the following code:-

        val t1 = Todo(0,"Todo1","A","B",true)
        val t2 = Todo(0,"Todo2","C","D", false)
        val t3 = Todo(0,"Todo3","E","F")
        val t4 = Todo(100,"Todo4","G","H",true)
        var t5 = Todo(101,"Todo5","I","J")
    
        todoDao.addTodo(t1)
        todoDao.addTodo(t2)
        todoDao.addTodo(t3)
        todoDao.addTodo(t4)
        todoDao.addTodo(t5)
        t5.title = "Something Else"
        t5.date = "Y"
        t5.title = "Z"
        t5.todoCheckBox = true
        todoDao.deleteSelectedTasks(arrayOf(t1,t2,t3,t4,t5))
    

    Results in (via App Inspection) :-

    enter image description here

    You may notice that only Todo4 and Todo5 have been deleted. The other 3 have not. This is actually intended to demonstrate how @Delete works.

    The reason why only Todo4 and Todo5 have been deleted is that t4 and t5 have the actual id hard-coded. That is @Delete works by deleting according to the Primary Key (the id in your case) the other values are irrelevant (as is shown by changing all the other t5 values)

    As proof that t4 and t5 were added (as you use autogenerate=true) then sqlite_sequence (the table that stores the highest used sequence (aka id)) shows 101 (t5's id) :-

    enter image description here

    Your Issue

    As such your issue is that you are not setting the appropriate id value when building the list of items to be deleted. As such it will be 0 and there will be no rows with an id of 0 and hence no rows will be deleted (like t1-t3 in the demo above).

    Additional as per the comment you could use:-

    @Query("DELETE FROM todo_table WHERE id IN (:idList)")
    fun deleteByIdList(idList: List<Long>)
    
    • this is effectively what Room builds and runs (I believe) the id's being obtained from the passed List/Array of Todo's.

    Without the id's (you may not have them in the layout (perhaps have them hidden in layout)) you could use BUT ONLY IF ALL OTHER VALUES COMBINED WILL WITHOUT DOUBT BE UNIQUE use the following combination of functions:-

    /* Note that this Query/Function is intended to be used by the following deleteByTodoListValues function */
    @Query("DELETE FROM todo_table WHERE title=:title AND date=:date AND time=:time AND todoCheckBox=:todoCheckBox")
    fun deleteByValuesOtherThanId(title: String, date: String, time: String, todoCheckBox: Boolean)
    
    @Transaction /* Transaction so that all deletes are done in a single transaction */
    @Query("") /* I believe this fools Room into accepting the transaction processing */
    fun deleteByTodoListValues(todoList: Array<Todo>) {
        for (t in todoList) {
            deleteByValuesOtherThanId(t.title,t.date,t.time,t.todoCheckBox)
        }
    }
    
    • A transaction equates to a disk write without @Transaction then each delete would be a disk write