Advertisement
  1. Code
  2. Mobile Development
  3. Android Development

How to Add Multiple Selection to Android RecyclerView

Scroll to top

In one of our earlier tutorials, we taught you how to display of list of items to your users with the help of the RecyclerView and CardView widgets. In this tutorial, we will expand on the concepts learned in that tutorial to learn how to select and track list items.

Initial Setup

We will begin by creating a new Android project by clicking the New Project button. Choose Empty Activity and then click the Next button.

Recycler Selection New ProjectRecycler Selection New ProjectRecycler Selection New Project

On the next screen, set the project name to whatever you want. I've set it to RecyclerSelection. Make sure that the language is Kotlin and that the minimum SDK value is 23.

Recycler Selection Project SettingsRecycler Selection Project SettingsRecycler Selection Project Settings

The ability to select any items and control their visual representation, along with other things like selection eligibility and how many items can be selected, is implemented through the recyclerview-selection library. Therefore, you need to open your build.gradle file and add the following dependencies:

1
implementation 'androidx.recyclerview:recyclerview:1.3.0'
2
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'

Creating Our List

We will now write some XML and Kotlin code to display a list of to-do tasks in our app. Create a file called todo_task.xml by navigating to Layout > New > Layout Resource File. It should have the following XML:

1
<?xml version="1.0" encoding="utf-8"?>
2
<androidx.cardview.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android"
3
    xmlns:app="http://schemas.android.com/apk/res-auto"
4
    xmlns:tools="http://schemas.android.com/tools"
5
    android:id="@+id/card_view"
6
    android:layout_width="match_parent"
7
    android:layout_height="80dp"
8
    android:layout_marginTop="10dp"
9
    android:layout_marginStart="20dp"
10
    android:layout_marginEnd="20dp"
11
    app:cardCornerRadius="4dp" >
12
13
        <LinearLayout
14
            android:layout_width="match_parent"
15
            android:layout_height="wrap_content"
16
            android:layout_marginStart="20dp"
17
            android:layout_marginTop="10dp"
18
            android:orientation="vertical">
19
20
            <TextView
21
                android:id="@+id/task_title"
22
                android:layout_width="wrap_content"
23
                android:layout_height="wrap_content"
24
                tools:text="Buy Some Apples"
25
                android:fontFamily="sans-serif-black"
26
                android:textColor="@color/black"
27
                android:textAllCaps="true"
28
                android:textSize="24sp" />
29
30
            <TextView
31
                android:id="@+id/task_detail"
32
                android:layout_width="wrap_content"
33
                android:layout_height="wrap_content"
34
                tools:text="Make sure they are fresh!"
35
                android:fontFamily="sans-serif-condensed-medium"
36
                android:textColor="@color/teal_700"
37
                android:textSize="18sp" />
38
39
        </LinearLayout>
40
41
</androidx.cardview.widget.CardView>

I have gone ahead and used a CardView widget in my layout file. However, you can just use a LinearLayout if you want, without wrapping it inside a CardView. This layout file defines how individual items in our to-do task will appear.

Now add the following XML to the activity_main.xml file.

1
<?xml version="1.0" encoding="utf-8"?>
2
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
    xmlns:app="http://schemas.android.com/apk/res-auto"
4
    xmlns:tools="http://schemas.android.com/tools"
5
    android:layout_width="match_parent"
6
    android:layout_height="match_parent"
7
    tools:context=".MainActivity">
8
9
    <androidx.recyclerview.widget.RecyclerView
10
        android:id="@+id/recycler_view"
11
        android:layout_width="match_parent"
12
        android:layout_height="match_parent"
13
        app:layout_constraintTop_toTopOf="parent"
14
        app:layout_constraintBottom_toBottomOf="parent" />
15
16
</androidx.constraintlayout.widget.ConstraintLayout>

The main layout file simply contains a RecyclerView widget because that's all we need.

This time, our data class will be called TaskItem, and the adapter class will be called RVAdapter because it is just an adapter for our RecyclerView.

1
data class TaskItem(val title: String, val description: String)
2
3
class RVAdapter(private val listItems: List<TaskItem>) :
4
    RecyclerView.Adapter<RVAdapter.TaskViewHolder>() {
5
6
    class TaskViewHolder(todoTaskView: View) : RecyclerView.ViewHolder(todoTaskView) {
7
        val title: TextView = todoTaskView.findViewById(R.id.task_title)
8
        val description: TextView = todoTaskView.findViewById(R.id.task_detail)
9
    }
10
11
    override fun getItemId(position: Int): Long = position.toLong()
12
13
    override fun getItemCount(): Int = listItems.size
14
15
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): TaskViewHolder {
16
        val v: View =
17
            LayoutInflater.from(viewGroup.context).inflate(R.layout.todo_task, viewGroup, false)
18
        return TaskViewHolder(v)
19
    }
20
21
    override fun onBindViewHolder(taskViewHolder: TaskViewHolder, position: Int) {
22
        taskViewHolder.title.text = listItems[position].title
23
        taskViewHolder.description.text = listItems[position].description
24
    }
25
}

At this point, our code is pretty much similar to the previous tutorial. Only the syntax is more concise. The RVAdapter class creates and binds view holders for each task. You should consider reading our previous tutorial to learn the basics of using RecyclerView if any of the above code looks confusing.

We have added a new method called getItemId() that is supposed to generate a stable ID for any given item. This method uses the position of the item to generate this ID. This is acceptable here because we are not deleting or adding items to the list.

You should also add the following code to the MainActivity class:

1
class MainActivity : AppCompatActivity() {
2
3
    private val myList = mutableListOf(
4
        TaskItem("Get a Haircut", "Keep Hair Short"),
5
        TaskItem("Go to The Park", "Take Tubby With You"),
6
        TaskItem("Buy Some Apples", "Make Sure They are Fresh"),
7
    )
8
9
    override fun onCreate(savedInstanceState: Bundle?) {
10
        super.onCreate(savedInstanceState)
11
        setContentView(R.layout.activity_main)
12
13
        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
14
        recyclerView.setHasFixedSize(true)
15
16
        recyclerView.layoutManager = LinearLayoutManager(this)
17
18
        val adapter = RVAdapter(myList)
19
        recyclerView.adapter = adapter
20
    }
21
22
}

Run the application at this point, and you will see a simple list of three to-do tasks.

Implementing the Selection Functionality

Long presses on any list item will not result in their selection at this point. However, that functionality is easy to add. We will begin by updating our RVAdapter class to include a tracker.

Update the TaskViewHolder class by adding a getItemDetails() method as shown below:

1
class TaskViewHolder(todoTaskView: View) : RecyclerView.ViewHolder(todoTaskView) {
2
    val title: TextView = todoTaskView.findViewById(R.id.task_title)
3
    val description: TextView = todoTaskView.findViewById(R.id.task_detail)
4
5
    fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
6
        object : ItemDetailsLookup.ItemDetails<Long>() {
7
            override fun getPosition(): Int = bindingAdapterPosition
8
            override fun getSelectionKey(): Long = itemId
9
        }
10
}

The getItemDetails() method returns an anonymous object that extends ItemDetailsLookup.ItemDetails<Long>. It overrides the getPosition() and getSelectionKey() methods to return the position and selection key of the item associated with our view holder.

It might sound confusing now, but we will use the getItemDetails() method later to determine which item was clicked by the user.

There is a getItemId() method in our RVAdapter class. Add the following code above this method:

1
init {
2
    setHasStableIds(true)
3
}
4
5
private var tracker: SelectionTracker<Long>? = null
6
7
fun setTracker(tracker: SelectionTracker<Long>?) {
8
    this.tracker = tracker
9
}

The code inside the init block is executed whenever RVAdapter is instantiated. We use this block to explicitly state that all the adapter items will have stable IDs. In other words, the IDs won't change even if their position within the dataset changes.

We also define a tracker property and use the setTracker() method to set its value.

Now, update the onBindViewHolder() method to have the following code:

1
override fun onBindViewHolder(taskViewHolder: TaskViewHolder, position: Int) {
2
    taskViewHolder.title.text = listItems[position].title
3
    taskViewHolder.description.text = listItems[position].description
4
5
    val parentCard = taskViewHolder.title.parent.parent as CardView
6
7
    tracker?.let {
8
        if (it.isSelected(position.toLong())) {
9
            parentCard.background = ColorDrawable(Color.LTGRAY)
10
        } else {
11
            parentCard.background = ColorDrawable(Color.WHITE)
12
        }
13
    }
14
}

The onBindViewHolder() method binds data from the adapter's dataset to a view holder. In this case, it simply sets the text value for the title and description views in the first two lines. After that, it gets a reference to the parent CardView of the concerned title.

Next, we call the let function on our tracker object. It lets us execute a block of code, but only if the calling object is not null. We use this function to check if the item at the given position is selected to see if its color should be changed to light gray.

Now, we will write the code for our ItemLookup class, which will help our tracker determine which item was clicked by the user.

1
class ItemLookup(private val rv: RecyclerView) : ItemDetailsLookup<Long>() {
2
    override fun getItemDetails(event: MotionEvent)
3
            : ItemDetails<Long>? {
4
        val view = rv.findChildViewUnder(event.x, event.y)
5
        if (view != null) {
6
            return (rv.getChildViewHolder(view) as RVAdapter.TaskViewHolder)
7
                .getItemDetails()
8
        }
9
        return null
10
    }
11
}

We are overriding the getItemDetails() method of the ItemDetailsLookup class inside our ItemLookup class. The returned ItemDetails object of this method contains the details about the item that was touched.

The MotionEvent argument passed to getItemDetails() is used to find the child view which was clicked. After that, we obtain a reference to the child's view holder and call its getItemDetails() method. The getItemDetails() method returns an ItemDetails object which is ultimately returned by the getItemDetails() method of the ItemLookup class.

Updating the onCreate() Method to Track Selections

At this point, we are ready to add the tracking functionality to the onCreate() method of our MainActivity class. Update your onCreate() method so that it has the following code:

1
override fun onCreate(savedInstanceState: Bundle?) {
2
    super.onCreate(savedInstanceState)
3
    setContentView(R.layout.activity_main)
4
5
    val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
6
    recyclerView.setHasFixedSize(true)
7
8
    recyclerView.layoutManager = LinearLayoutManager(this)
9
10
    val adapter = RVAdapter(myList)
11
    recyclerView.adapter = adapter
12
13
    tracker = SelectionTracker.Builder(
14
        "selection-1",
15
        recyclerView,
16
        StableIdKeyProvider(recyclerView),
17
        ItemLookup(recyclerView),
18
        StorageStrategy.createLongStorage()
19
    ).withSelectionPredicate(
20
        SelectionPredicates.createSelectAnything()
21
    ).build()
22
23
    savedInstanceState?.let {
24
        tracker?.onRestoreInstanceState(it)
25
    }
26
27
    adapter.setTracker(tracker)
28
29
    tracker?.addObserver(
30
        object : SelectionTracker.SelectionObserver<Long>() {
31
            override fun onSelectionChanged() {
32
                val nItems: Int? = tracker?.selection?.size()
33
34
                nItems?.let {
35
                    if (it > 0) {
36
                        title = "$it items selected"
37
                        supportActionBar?.setBackgroundDrawable(
38
                            ColorDrawable(getColor(R.color.orange_700))
39
                        )
40
                    } else {
41
                        title = "RecyclerSelection"
42
                        supportActionBar?.setBackgroundDrawable(
43
                            ColorDrawable(getColor(R.color.purple_500))
44
                        )
45
                    }
46
                }
47
            }
48
        })
49
}

We initialize our tracker using the SelectionTracker.Builder class. The constructor takes several arguments: a selection ID to uniquely identify the selection, the RecyclerView where the selection is happening, a key provider, your item details lookup class, and a storage strategy.

You need to provide a storage strategy so that the active selection isn't lost whenever the activity configuration changes. Our selection keys are of the Long type. Therefore, it is important that our storage strategy also creates storage for the Long type.

Once the Builder is ready, you can call its withSelectionPredicate() method to specify how many items you want to allow the user to select. In order to support multi-item selection, as an argument to the method, you must pass the SelectionPredicate object returned by the createSelectAnything() method.

If the savedInstanceState is not null, we also use the let function to call the onRestoreInstanceState() method on our tracker object. This restores the selection state of the tracker from its saved state.

The selection tracker is not very useful unless it is associated with your adapter. Therefore, pass it to the adapter by calling the setTracker() method. We have already defined this method in the previous section.

You will probably want to take different actions in your app depending on the number of selection items. We can use the addObserver() method on our tracker object to observe any changes in the selection. After that, we override the onSelectionChanged() method to change the title and background color of the action bar. This lets the users know that there is an active selection in the list and how many items have been selected.

Unselected and Selected List Items (RecyclerView)Unselected and Selected List Items (RecyclerView)Unselected and Selected List Items (RecyclerView)

Saving the Activity State

Just like the onCreate() method, which is invoked when an activity is first created, there is a method called onSaveInstanceState() that is called when the activity is about to be destroyed either due to a configuration change or to reclaim memory. We want to save the state of our activity and our selection at this point. Therefore, we will override this method using the following code and place it below the onCreate() method.

1
override fun onSaveInstanceState(outState: Bundle) {
2
    super.onSaveInstanceState(outState)
3
    tracker?.onSaveInstanceState(outState)
4
}

How the Code Works Together

We have divided the code for the entire app into multiple parts to explain how it works. However, this can make it hard to figure out how the whole thing fits together. I will now explain how different activities and methods are connected.

When a user launches the app, it creates an instance of the MainActivity class and calls the onCreate() method. All our initial setup is done inside the onCreate() method. This includes initializing the user interface and setting up the RecyclerView as well as its adapter with the help of the RVAdapter class. The RVAdapter class is needed to create and bind a view holder to each of our tasks.

The SelectionTracker is also set up inside onCreate() itself. We pass this tracker to the adapter using the setTracker() method. The adapter relies on the SelectionTracker to determine which tasks have been selected and updates their background accordingly.

The SelectionTracker needs the ItemLookup class to determine which item was clicked by the user. The SelectionTracker updates its observer about any change in the selection state of different items. This observer was attached to our tracker when the activity was created, and it updates the action bar title and color based on the selected items.

You might have noticed that there are two getItemDetails() methods. The first one is inside the TaskViewHolder class, and the second one is inside the ItemLookup class. However, they both serve different purposes.

The getItemDetails() method inside TaskViewHolder returns information about the item associated with the view holder. However, the getItemDetails() method inside ItemLookup returns information about the item that was clicked. It does so by first calling findChildViewUnder() to determine which item is under the user touch event and ultimately calling the getItemDetails() method of the TaskViewHolder class.

The code inside the onSaveInstanceState() method saves the state of the activity and the selection before it is destroyed by the system. We restore this state later, inside the onCreate() method, if the savedInstanceState is not null.

Final Thoughts

In this tutorial, you learned how to use the RecyclerView Selection addon library to add simple item selection support to a RecyclerView widget. You also learned how to dynamically alter the appearance of selected items so that users can tell them apart from unselected ones.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.