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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
RecyclerView Selection Library Tutorial for Android: Adding New Actions
20 mins
- Getting Started
- Understanding the Project Structure
- Implementing Contextual Action Mode
- Getting to Know RecyclerView Selection
- Adding Selection to Your Existing RecyclerView
- Implementing ItemKeyProvider
- Implementing ItemDetailsLookup
- Updating MainAdapter
- Implementing a SelectionTracker
- Updating MainAdapter Initialization
- Updating ActionMode Callbacks
- Saving and Restoring State
- Where to Go From Here?
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:
- You extend
ItemDetailsLookup
and useLong
as the default type. Following the same logic as when you created ItemsKeyProvider, it corresponds to theid
of the Item. - 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. - To retrieve the view that corresponds to the
MotionEvent
, you callfindChildViewUnder
from the RecyclerView that, using thex
andy
coordinates, returns the view in that position, in case it exists. - You return
ItemDetails
from thegetItem
call in case theview
exists, ornull
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:
- To implement
ItemDetailsLookup
, you once again need to define its type. Because theid
is defined asLong
, you need to use the class inItemDetails
. - 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
. - When you call
getSelectionKey
, it returns the correspondingid
(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:
-
selectionItem
corresponds to the unique identifier for this SelectionTracker. - The RecyclerView where it’s going to be applied.
- The ItemsKeyProvider and ItemsDetailsLookup you’ve created before.
- The StorageStrategy you’ll use to store the keys. Because you’re using Long, you need to use
createLongStorage
. - 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.
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