How to delete a crime in chapter 15 challenge?

How do you delete a crime?
I put a delete icon on the menu. This is easy. But how do you tell it to delete the item you click on the recyclerview? Further more, when you click on an item, you are presented with a new fragment to edit it.

Anyway, I have tried the selection library, following some tutorials on the Internet, but they all use the key of type Long and the crime uses type UUID. Is there a way to convert it to type Long?

The challenge is asking too much. Please provide more guidance.

1 Like

I do not want to give away the entire solution, but I will give some hints that will hopefully point you in the right direction. I do not think adding a delete icon to the App Bar is the best way forward. Adding a Button to the list_item_crime.xml is probably a better way forward. Then you could probably hook up something similar to what you did in order to make the navigation work.

As far as the Room portion of this challenge goes, you will not be able to use a Long because of the reason you mentioned. But if you look at Google’s documentation (Accessing data using Room DAOs  |  Android Developers), you see that Room can accept an @Entity annotated class as a parameter on function to delete an entry. Room will match that entity based on the @PrimaryKey (which is that UUID in Crime). All the other properties do not matter, so you could do something like:

val byeByeCrime = Crime(idToDelete, "", Date(), false)
database.crimeDao().deleteCrime(byeByeCrime)

I hope this points you in the right direction.

Thank you for the quick answer. Reading the hints in the book, I thought readers were required to code so that when a user long presses an item a popup menu appears and asks you what to do as we all do with our apps.
This is going to be much much easier than the selection libary.

Not easy as I thought.
Now that I have added delete button to list_item_crime.xml

I have changed fun bind to read:

       binding.crimeTitle.setOnClickListener {
            onCrimeClicked(crime.id)
        }

        binding.deleteButton.setOnClickListener {
                onCrimeClicked(crime.id)
        }

I am stuck with how to tell recyclerview to call viewmodel.deleteCrime and not navigate to the edit fragment.

Could you please give some more hints.

Finally, I made it work but do you think this is the right way.
I changed the adapter signature, adding onCrimeDelete

class CrimeListAdapter(
private val crimes: List,
private val onCrimeClicked: (crimeId: UUID) → Unit,
private val onCrimeDelete: (crimeId: UUID) → Unit

and in CrimeListFragment

    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            crimeListViewModel.crimes.collect { crimes ->
                binding.crimeRecyclerView.adapter = CrimeListAdapter(
                    crimes,
                    onCrimeClicked = { crimeId ->
                        findNavController().navigate(
                            CrimeListFragmentDirections.showCrimeDetail(crimeId)
                        )
                    },
                    onCrimeDelete = { crimeId ->
                        val crime = Crime(crimeId, "", Date(), false)
                        viewLifecycleOwner.lifecycleScope.launch {
                            crimeListViewModel.deleteCrime(crime)
                        }
                    }
                )
            }
        }
    }

Is this how you would do it or is there a better way?

Thank you.

P.S
I must call it like this:

viewLifecycleOwner.lifecycleScope.launch {
crimeListViewModel.deleteCrime(crime)
}

Why cant you call crimeListViewModel.deleteCrime(crime)? I think we can call a suspend function from with another.

Not easy as I thought. But still thank you admin by sharing!

Yes this is how I would do it.

You are on the right track with the idea that you need to be inside a coroutine scope in order to call suspending functions. And with the repeatOnLifecycle() call, you do have a coroutine scope, but you are calling the crimeListViewModel.deleteCrime(crime) line from within a lambda expression passed into CrimeListAdapter. That lambda expression (onCrimeDelete) is actually invoked inside the CrimeHolder. And where it is invoked is not within a coroutine scope, so you can’t call suspending functions there.

I hope that helped.

Thank you very much for the detailed answer.

I have a further question about using the recyclerview selection to delete a crime. I have been thinking about this for some time but just not been able to find out why it does not work. Thank you very much for reading.

Here is my selectionTracker:

  viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            crimeListViewModel.crimes.collect { crimes ->
           // val crimes = crimeListViewModel.crimes
             recyclerviewAdapter = CrimeListAdapter(
                crimes,
                onCrimeClicked = { crimeId ->
                    findNavController().navigate(
                        CrimeListFragmentDirections.showCrimeDetail(
                            crimeId
                        )
                    )
                }, onCrimeDelete = { crimeId ->
                    viewLifecycleOwner.lifecycleScope.launch {
                        val crime = Crime(crimeId, "", Date(), false)
                        crimeListViewModel.deleteCrime(crime)
                    }
                }
            )
                binding.crimeRecyclerView.adapter = recyclerviewAdapter

                selectionTracker = SelectionTracker.Builder(
                    "my-selection",
                    binding.crimeRecyclerView,
                    CrimeKeyProvider(recyclerviewAdapter),
                    CrimeDetailsLookup(binding.crimeRecyclerView),
                    StorageStrategy.createParcelableStorage(Crime::class.java)
                ).withSelectionPredicate(createSelectAnything())
                    .build()

This will not work. And here is the error:

java.lang.IndexOutOfBoundsException: Empty list doesn’t contain element at index 0.

So I guess the adapter is not updated once it is passed to the selectorTracker.
The selection only works if I passed the StateFlow<List> directly to the adapter like this:

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
val crimes: StateFlow<List> = crimeListViewModel.crimes
recyclerviewAdapter = CrimeListAdapter(
crimes,
…}

Though the selection works, the crimetext will not appear after you added it. And you have to click on it for the text to show.

I have never worked with SelectionTracker, so unfortunately I cannot provide any help here. Please let me know if you figure out the solution on your own.

This is my solution for deleting a crime, In CrimeListFragment I declare a var:
private var crimeForDeletion = false
In the menu section I set a Delete icon with this script:

override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
                // Handle the menu selection
                return when (menuItem.itemId) {
                    R.id.new_crime -> {
                        showNewCrime()
                        true
                    }
                    R.id.delete_crime -> {
                        crimeForDeletion = true
                        Toast.makeText(
                            requireContext(),
                            "SELECT CRIME TO DELETE",
                            Toast.LENGTH_LONG
                        ).show()
                        true
                    }
                    else -> false
                }
            }
        }, viewLifecycleOwner, Lifecycle.State.RESUMED)

When the adapter is called, there are 2 options, one to delete the crime to be clicked on or to view the crime:

binding.crimeRecyclerView.adapter = CrimeListAdapter(crimes) { crimeId ->

                        if (crimeForDeletion) {
                            deleteCrime(crimeId)
                            crimeForDeletion = false
                        } else {
                            findNavController().navigate(
                                CrimeListFragmentDirections.showCrimeDetail(crimeId)
                            )
                        }
                    }

The script inside the deleteCrime is:

private fun deleteCrime(uid: UUID) {
        viewLifecycleOwner.lifecycleScope.launch {

            val delCrime = Crime(
                id = uid,
                title = "",
                date = Date(),
                isSolved = false
            )
            crimeListViewModel.deleteCrime(delCrime)
        }
    }

Instead of deleting the crime by pressing on it, one can put an alertdialog as intermediate:

                     if (crimeForDeletion) {
                            showAlert(crimeId)
                            crimeForDeletion = false
                        }
_________________

private fun showAlert(uid: UUID) {
        alertDialog = context?.let { AlertDialog.Builder(it) }
        val dialogView: View = layoutInflater.inflate(R.layout.place_holder, null)
        alertDialog!!.setView(dialogView)
        dialog = alertDialog!!.create()
        dialog!!.show()
        val delCrime: TextView = dialogView.findViewById(R.id.ok_but)
        delCrime.setOnClickListener {
            deleteCrime(uid)
        }
        val cancelDel: TextView = dialogView.findViewById(R.id.cancel_but)
        cancelDel.setOnClickListener {
            dialog!!.hide()
        }
    }

I also gave the possibility to delete a crime, when it is viewed from inside the CrimeDetailFragment.