RecyclerView Selection Library Tutorial for Android: Adding New Actions

Learn how to implement press and long-press events on lists with RecyclerView Selection library. By Carlos Mota.

5 (1) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Implementing ItemDetailsLookup

Every time there’s a touch interaction with the list, the selection library needs to know which item it should interact with. To achieve that, you’ll need to extend the ItemDetailsLookup class and define getItemDetails. This is the method that’s going to trigger internally.

In the adapters package, create a second file namedItemsDetailsLookup.kt. Here, add:

//1
class ItemsDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
 
 //2
 override fun getItemDetails(event: MotionEvent): ItemDetails<Long>? {
   //3
   val view = recyclerView.findChildViewUnder(event.x, event.y)
   if (view != null) {
   //4
     return (recyclerView.getChildViewHolder(view) as MainAdapter.ItemViewHolder).getItem()
   }
   return null
 }
}

When prompted for imports, use the following:

import android.view.MotionEvent
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.widget.RecyclerView

In the code above:

  1. You extend ItemDetailsLookup and use Long as the default type. Following the same logic as when you created ItemsKeyProvider, it corresponds to the id of the Item.
  2. The selection library triggers this method when a motion event occurs. Here, you return the view the user interacted with or null in case there was no motion event.
  3. To retrieve the view that corresponds to the MotionEvent, you call findChildViewUnder from the RecyclerView that, using the x and y coordinates, returns the view in that position, in case it exists.
  4. You return ItemDetails from the getItem call in case the view exists, or null otherwise.

But when you call getItem, you see there’s no corresponding method. Because it’s part of the adapter, open MainAdapter.kt and, inside the ItemViewHolder inner class, add:

fun getItem(): ItemDetailsLookup.ItemDetails<Long> =
 
     //1
     object : ItemDetailsLookup.ItemDetails<Long>() {
       
       //2
       override fun getPosition(): Int = bindingAdapterPosition
      
       //3
       override fun getSelectionKey(): Long = items[bindingAdapterPosition].id
     }
}

Here’s a logic breakdown:

  1. To implement ItemDetailsLookup, you once again need to define its type. Because the id is defined as Long, you need to use the class in ItemDetails.
  2. The item position in the adapter is returned. Remember this function is part of the ViewHolder, so it’s going to return the position of the view that called getPosition.
  3. When you call getSelectionKey, it returns the corresponding id (selection key) for that view.

Now that you’ve defined all the missing classes, it’s time to reimplement the actions the grocery list is going to have, this time using the selection library.

Updating MainAdapter

First, you’ll remove all the code that is no longer necessary. Start by deleting the cb package that contains the IAction.kt file.

Doing so triggers a couple errors in the project, so it’s time to fix them.

Open the MainAdapter.kt file and update the class declaration to:

class MainAdapter(
  private val action: (items: List<Item>, changed: Item, checked: Boolean) -> Unit
) : RecyclerView.Adapter<MainAdapter.ItemViewHolder>()

You no longer need to have an interface, so you can just send this lambda expression.

Remember to also remove the IAction import, which no longer exists.

Instead of having a list of selected elements, you’ll use the SelectionTracker from the selection library. To accomplish that, remove this property and add:

var tracker: SelectionTracker<Long>? = null

This will manage the item selection.

Because you no longer use the selected property, you need to also remove clearSelection.

Now, go to bind inside the ItemViewHolder and update the setOnCheckedChangeListener callback to:

itemBinding.cbItem.setOnCheckedChangeListener { _, isChecked ->
 if (item.id == COOKIE_ID) {
   itemBinding.cbItem.isChecked = false
   action(items, item, false)
 
 } else {
   action(items, item, isChecked)
 }
}

Instead of calling onItemUpdate, you’ll now call action.

The SelectionTracker manages the list item selections automatically, so you no longer need to declare the setOnClickListener and the setOnLongClickListener.

Remove these two invocations and replace them with:

tracker?.let {
 
 if (it.isSelected(item.id)) {
   itemBinding.cbItem.setBackgroundColor(
       ContextCompat.getColor(itemBinding.cbItem.context, R.color.colorPrimary60))
 } else {
   itemBinding.cbItem.background = null
 }
}

All the logic that determines which views used to be selected is now handled automatically by the selection library. Here, you define the row color for when the state changes: It will be green if the user selects a new item, or there will be no background if they deselect.

Because you’re setting the background here, you can remove the setSelectedViewStyle that’s defined above the setOnCheckedChangeListener. With this, there’s more to delete at the end of this file. You can remove methods isItemSelected and updateSelectedItem. Also, make sure to remove the setSelectedViewStyle reference in bind.

Removing setOnClickListener allows you to simplify the XML layout. Open item_grocery.xml and replace with:

<CheckBox
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/cb_item"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:paddingStart="8dp"
  android:paddingEnd="16dp"
  android:paddingTop="16dp"
  android:paddingBottom="16dp"
  android:fontFamily="sans-serif"
  android:text="@string/app_name"
  android:textSize="15sp"
  android:theme="@style/CheckBoxStyle" />

You only need to listen to OnCheckedChangeListener; you no longer need the container.

Implementing a SelectionTracker

Now that the adapter is updated, open MainFragment.kt.

Start by doing the same process and remove IAction from the import and classes extensions.

Next, declare a SelectionTracker in MainFragment.kt, which is going to be responsible for managing the selected items. After the binding declaration, add:

private lateinit var tracker: SelectionTracker<Long>

Next, define it inside the setupUiComponents function. After the setOnClickListener definition on ivAddToCart, add:

tracker = SelectionTracker.Builder(
   //1
   "selectionItem",
   //2   
   binding.rvGroceries,
   //3
   ItemsKeyProvider(mainAdapter),
   ItemsDetailsLookup(binding.rvGroceries),
   //4
   StorageStrategy.createLongStorage()
).withSelectionPredicate(
   //5
   SelectionPredicates.createSelectAnything()
).build()

Here’s this logic breakdown:

  1. selectionItem corresponds to the unique identifier for this SelectionTracker.
  2. The RecyclerView where it’s going to be applied.
  3. The ItemsKeyProvider and ItemsDetailsLookup you’ve created before.
  4. The StorageStrategy you’ll use to store the keys. Because you’re using Long, you need to use createLongStorage.
  5. The SelectionPredicates define the rules for when an item can be selected. Using createSelectAnything allows the user to select one or more items without any constraints.
Note: As an alternative to createSelectAnything, you could use createSelectSingleAnything, where only one item can be selected. To see how the app behaves, after this section, change the SelectionPredicate to this mode.

Add the observer to listen to any selection change:

tracker.addObserver(
   object : SelectionTracker.SelectionObserver<Long>() {
     override fun onSelectionChanged() {
       super.onSelectionChanged()
 
       if (actionMode == null) {
         val currentActivity = activity as MainActivity
         actionMode = currentActivity.startSupportActionMode(this@MainFragment)
 
         binding.etNewItem.clearFocus()
         binding.etNewItem.isEnabled = false
       }
 
       val items = tracker.selection.size()
       if (items > 0) {
         actionMode?.title = getString(R.string.action_selected, items)
       } else {
         actionMode?.finish()
       }
     }
   })

If you look closely at this new code block, you see it’s the same one you had onItemAction.

Instead of implementing the press and long-press actions, you can keep track of what changes via the onSelectionChanged callback and then update the actionMode accordingly.

Finally, add tracker to the mainAdapter:

mainAdapter.tracker = tracker