Last Chance: RW Accelerator Bootcamps

Bootcamps start August 15th — Don't miss out!
Save 50% and go from novice coder to job-ready mobile developer in just 12 weeks.

Home Android & Kotlin Tutorials

RecyclerView Selection Library Tutorial for Android: Adding New Actions

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

Version

  • Kotlin 1.5, Android 4.4, Android Studio 2020.3.1

Android RecyclerViews are among the most used components throughout Android apps. They also are among the most powerful ones, optimized to display large sets of information on which users can easily browse.

Due to its popularity, a set of libraries allows you to empower RecyclerViews even more and give them additional functionalities. One of those is the selection library, which allows easy implementation of press and long-press events on lists.

In this tutorial, you’ll build a grocery list app. It uses the selection library to provide extra features the user can interact with, namely:

  • Pressing an item crosses it from the list.
  • Long-pressing an item triggers multiple-selection mode.
Note: This tutorial assumes you’re familiar with Android Studio and the basics of Android development. If you’re new to either, read Beginning Android Development and Kotlin for Android: An Introduction before continuing.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

You’ll find two projects inside the ZIP file. Starter has the skeleton of the app you’ll build, and Final gives you something to compare your code against when you’re done.

Build and run the app. You’ll see an app that consists of a shopping list prefilled with groceries:

Bring Cookies feature set: Add a new item, mark items as done and remove items

You’re going to create BringCookies, an app that helps you keep track of everything you need to buy. It comes with a bonus feature! The item Cookies is always on the list because there’s always room for more cookies. :]

Understanding the Project Structure

Open the Starter project in Android Studio and wait for it to synchronize.

The directory structure of the project

You’ll see a set of packages and other important files:

  • adapters: This contains the RecyclerView adapter and a set of utilities used to select items from the list.
  • model: It contains the Item data object used to represent an item. Item consists of id, its value, the timestamp of when it was created and done, the current state.
  • MainActivity: The app’s single activity.
  • MainFragment: It’s responsible for displaying the grocery list to the user and providing a set of mechanisms with which the user can interact.
  • Utils: This file contains a set of utility methods you’ll use throughout the project.
Note: In this tutorial, you’ll use RecyclerView to display your grocery list. To learn more advanced content about how to use this view, check out Android RecyclerView Tutorial with Kotlin or Intermediate RecyclerView Tutorial with Kotlin.

Moving on, it’s time to learn more about the selection library.

Implementing Contextual Action Mode

The contextual mode adds a set of functionalities you can apply to selected items. It lives in the app bar. To display the contextual action mode, the user needs to long-press an item.

The app you’re going to develop allows you to remove groceries from your list.

Open the MainFragment.kt file. In the class declaration, you can see you’re already implementing the ActionMode.Callback, which you use to enable/disable this feature.

Note: There’s also an actionMode declared that you use to check the current state of the action mode.

Here are the methods from this callback:

  • onCreateActionMode loads the correct resource file for the action mode menu.
  • onPrepareActionMode, set as true, ensures all the menu actions will update on every launch.
  • onActionItemClicked defines the actions on the items defined in the menu. In this case, there’s only one action defined — delete. When selected, all these items disappear from the current list, which is then saved locally.
  • onDestroyActionMode triggers when the user exits action mode. You can add new items again, and action mode disappears along with all the selected items.

Getting to Know RecyclerView Selection

To make it easier to interact with the items of RecyclerView, the Android team created the selection library. The starter project contains the logic necessary to implement the setOnClickListener and setOnLongClickListener events on a list.

Note: You’ll find the official documentation from the selection library here.

If you open MainAdapter.kt, you see it implements an interface, IAction, with two different methods:

  • onItemUpdate that’s going to be triggered every time the user clicks a checkbox, which marks an item as done.
  • onItemAction enables multi-selection and select items when this mode is on. When the user long-presses an item in the list, this feature gets activated. This feature de-activates when the user clicks either Back or Delete in the action bar.

For now, you need to do all these manually. If you open MainFragment.kt and look for the implementation of these two methods, you see:

  • onItemUpdate: When triggered, the current item state updates to done or not done, and this new value gets stored locally.
  • onItemAction: Depending on the current value of actionMode, it can be enabled via the invocation of startSupportActionMode: If no items are selected, it will be disabled by calling finish. While it’s on, every item click will update its title that corresponds to the number of items selected.

Adding Selection to Your Existing RecyclerView

To access the functionalities from RecyclerView selection, you need to first add the library to the build.gradle file inside the app folder:

implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'

Once you’ve added that, click Synchronize. Android Studio will download all the necessary resources so you can start implementing these new features!

Implementing ItemKeyProvider

With the library added to the project, it’s time to implement the ItemKeyProvider. This class holds the ID’s of the different cells you can select.

ItemKeyProvider currently supports: Parcelable, String and Long objects. You’re going to use the last one in this project.

Navigate to the adapters package, create the ItemsKeyProvider.kt file and add:

//1
class ItemsKeyProvider(private val adapter: MainAdapter) : ItemKeyProvider<Long>(SCOPE_CACHED) {

 //2
 override fun getKey(position: Int): Long =
     adapter.items[position].id

 //3
 override fun getPosition(key: Long): Int =
     adapter.items.indexOfFirst { it.id == key }
}

You’ll need to import: androidx.recyclerview.selection.ItemKeyProvider.

Here’s a step-by-step breakdown of this logic:

  1. The ItemsKeyProvider receives the adapter where it’s going to be used. You’ll use the parameter id from Item, which is defined as Long, so you’ll need to set and extend this type via ItemKeyProvider.

    You can use two scope types to initialize this class:

    • SCOPE_MAPPED allows access to all the data in the adapter. Typically, this is used when the user wants to select all items in a list with a single click instead of marking them individually.
    • SCOPED_CACHED provides access only to items that were recently bound. This fits into the scenario of a multi-selection option, similar to the grocery list you’re developing, and is the one you’re going to use.
  2. Returns the key of the selection element at a specific position.
  3. From a specific key, it returns its position in the list.

ItemKeyProvider declares both getKey and getPosition methods, which you need to define on this newly created class. This is required because the logic of retrieving the key, and its index, depend on the implementation of which parameter the developer decides to use as key.

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

Updating MainAdapter Initialization

Now that you defined tracker, you need to update the call to MainAdapter, which is no longer the IAction interface. Instead, it’s a lambda function containing the code from onItemUpdate.

Still in setupUiComponents, scroll up to the method declaration and replace the mainAdapter definition with:

val mainAdapter = MainAdapter { items: List<Item>, changed: Item, checked: Boolean ->
 var element = items.first { it.id == changed.id }
 val index = items.indexOf(element)
 
 element = if (index == 0) {
   Snackbar.make(binding.clContainer, R.string.item_more_cookies, Snackbar.LENGTH_SHORT).show()
   element.copy(done = false)
 
 } else {
   element.copy(done = checked)
 }
 
 val updatedItems = items.toMutableList().apply { this[index] = element }
 updateAndSave(updatedItems)
}

Like the onSelectionChanged callback, the MainAdapter initialization corresponds to the code from onItemUpdate. Every time an item state changes in the groceries list, this value updates in the local storage.

Updating ActionMode Callbacks

There are a few more things to update before you can recompile the project. Following the errors in red, in the code, you’ll need to update onActionItemClicked with this newly added logic.

To access the items that are currently selected, you no longer access a variable inside the adapter. Instead, you can now directly call the tracker you’ve just created.

Update selected to:

val selected = mainAdapter.items.filter {
 tracker.selection.contains(it.id)
}.toMutableList()

Because tracker holds the ID’s of the currently selected items, you need to filter the existing items list to return only the selected ones.

Moving forward, you also need to update onDestroyActionMode. Because clearSelection no longer exists, to remove all the items that are currently selected, you need to call instead:

tracker.clearSelection()

Now that you’re all set, delete the implementation of IAction at the class declaration and its functions. Scroll down to the end of the file and remove the onItemUpdate and onItemAction functions.

It’s time to finally build and run the app. Before your next trip to buy groceries, don’t forget to remove the items you no longer need. Long-press in the list, select one or more elements, and click Delete.

Select and remove several items from the groceries list

Saving and Restoring State

Now that you’ve got your selection library implemented, you can take an extra step to guarantee your items will still be selected across different lifecycle events.

If you select a couple items and then switch your screen to landscape, you see they all disappear. To avoid this, you need to override the onSaveInstanceState and onViewStateRestored methods in MainFragment.kt:

override fun onSaveInstanceState(outState: Bundle) {
 tracker.onSaveInstanceState(outState)
 super.onSaveInstanceState(outState)
}
 
override fun onViewStateRestored(savedInstanceState: Bundle?) {
 tracker.onRestoreInstanceState(savedInstanceState)
 if (tracker.hasSelection()) {
   actionMode = (activity as MainActivity).startSupportActionMode(this@MainFragment)
   actionMode?.title = getString(R.string.action_selected, tracker.selection.size())
 }
 super.onViewStateRestored(savedInstanceState)
}

In each of these methods, you need to call the corresponding method from tracker. The selection library will handle the rest automatically.

Build and run the app, select a couple items and rotate the device to landscape.

Bring Cookies rotate screen and select new items

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Congratulations! You’ve learned how to implement the selection library into your RecyclerView. You’ve added press and long-press events into your app.

This is just a small example of features you can use with RecyclerView. Other tutorials focus on how to speed up its performance for large lists or how to use the Paging library to only load the necessary information on the screen.

If you have any questions or comments, please join the discussion below.

Contributors

Comments

Reviews

More like this