The RecyclerView
widget is an integral part of most Android applications today. Ever since it was added to the Android support library in late 2014, it has eclipsed the ListView
widget as the most preferred widget for displaying large, complex lists. However, there’s one important feature missing in it: support for selecting and tracking list items. RecyclerView Selection, an addon library Google released in March this year, tries to fix this.
In this tutorial, I’ll show you how to use the new library to create an app that offers an intuitive interface for selecting multiple items in a list. Follow along with this Android RecyclerView multiple selection example, and you’ll learn some skills you can apply in your own apps.
Prerequisites
To follow along, you’ll need:
- the latest version of Android Studio
- a device or emulator running Android API level 23 or higher
1. Add RecyclerView Android Dependencies
To add the RecyclerView Selection library to your Android Studio project, mention the following implementation
dependencies in your app
module’s build.gradle file:
implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'com.android.support:recyclerview-selection:28.0.0'
2. Create a List
Throughout this tutorial, we’ll be working with a small list of items, each containing a person’s name and phone number.
To store the data of each list item, create a Kotlin data class called Person
and add two properties to it: name
and phone
.
data class Person(val name:String, val phone: String)
You can now go ahead and create a list of Person
objects in your main activity.
val myList = listOf( Person("Alice", "555-0111"), Person("Bob", "555-0119"), Person("Carol", "555-0141"), Person("Dan", "555-0155"), Person("Eric", "555-0180"), Person("Craig", "555-0145") )
3. Add a Recycler View to the Layout
We will, of course, be using a RecyclerView
widget to display the list. So add a <RecyclerView>
tag to your main activity’s layout XML file.
<android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/my_rv"> </android.support.v7.widget.RecyclerView>
To specify the layout of the list items, create a new XML file and call it list_item.xml. Inside it, add two TextView
widgets: one to display the name, and the other to display the phone number. If you use a LinearLayout
element to position the widgets, the contents of the XML file should look like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/list_item_name" style="@style/TextAppearance.AppCompat.Large"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/list_item_phone" style="@style/TextAppearance.AppCompat.Small"/> </LinearLayout>
4. Create a View Holder
You can think of a view holder as an object that contains references to the views present in the layout of the list items. Without it, the RecyclerView
widget will not be able to render the list items efficiently.
For now, you need a view holder that holds the two TextView
widgets you created in the previous step. So create a new class that extends the RecyclerView.ViewHolder
class, and initialize references to both the widgets inside it. Here’s how:
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) { val name: TextView = view.list_item_name val phone: TextView = view.list_item_phone // More code here }
Additionally, the RecyclerView
Selection
addon needs a method it can call to uniquely identify selected list items. This method ideally belongs to the view holder itself. Furthermore, it must return an instance of the ItemDetailsLookup.ItemDetails
class. Therefore, add the following code to the view holder:
fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> = object: ItemDetailsLookup.ItemDetails<Long>() { // More code here }
You must now override two abstract methods present in the ItemDetails
class. Start by overriding the getPosition()
method and returning the adapterPosition
property of the view holder inside it. The adapterPosition
property is usually nothing but the index of a list item.
override fun getPosition(): Int = adapterPosition
Next, override the getSelectionKey()
method. This method must return a key that can be used to uniquely identify a list item. To keep things simple, let’s just return the itemId
property of the view holder.
override fun getSelectionKey(): Long? = itemId
You are free to use any other technique for generating the selection key, so long as it generates unique values.
5. Handle User Touches
For the RecyclerView
Selection
addon to work correctly, whenever the user touches the RecyclerView
widget, you must translate the coordinates of the touch into an ItemDetails
object.
Create a new class that extends the ItemDetailsLookup
class, and add a constructor to it that can accept the RecyclerView
widget as an argument. Note that, because the class is abstract, Android Studio will automatically generate a stub for its abstract method.
class MyLookup(private val rv: RecyclerView) : ItemDetailsLookup<String>() { override fun getItemDetails(event: MotionEvent) : ItemDetails<String>? { // More code here } }
As you can see in the above code, the getItemDetails()
method receives a MotionEvent
object. By passing the event’s X and Y coordinates to the findChildViewUnder()
method, you can determine the view associated with the list item the user touched. To convert the View
object into an ItemDetails
object, all you need to do is call the getItemDetails()
method. Here’s how:
val view = rv.findChildViewUnder(event.x, event.y) if(view != null) { return (rv.getChildViewHolder(view) as MyViewHolder) .getItemDetails() } return null
6. Create an Adapter
You’ll now need an adapter that can bind your list to your RecyclerView
widget. To create one, create a new class that extends the RecyclerView.Adapter
class. Because the adapter needs access to the list and your activity’s context, the new class must have a constructor that can accept both as arguments.
class MyAdapter(private val listItems:List<Person>, private val context: Context) : RecyclerView.Adapter<MyViewHolder>() { }
It is important that you explicitly indicate that each item of this adapter is going to have a unique stable identifier that is of type Long
. The best place to do so is inside an init
block.
init { setHasStableIds(true) }
Additionally, to be able to use the position of an item as its unique identifier, you’ll have to override the getItemId()
method.
override fun getItemId(position: Int): Long { return position.toLong() }
Because the RecyclerView.Adapter
class is abstract, you’ll now have to override three more methods to make your adapter usable.
First, override the getItemCount()
method to return the size of the list.
override fun getItemCount(): Int = listItems.size
Next, override the onCreateViewHolder()
method. This method must return an instance of the view holder class you created earlier in this tutorial. To create such an instance, you must call the constructor of the class and pass the inflated layout of your list items to it. To inflate the layout, use the inflate()
method of the LayoutInflater
class. Here’s how:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder = MyViewHolder( LayoutInflater.from(context) .inflate(R.layout.list_item, parent, false) )
Lastly, override the onBindViewHolder()
method and appropriately initialize the text
property of both the TextView
widgets present in the view holder.
override fun onBindViewHolder(vh: MyViewHolder, position: Int) { vh.name.text = listItems[position].name vh.phone.text = listItems[position].phone }
7. Display the List
At this point, you have almost everything you need to render your list. However, you must still specify how you want the list items to be positioned. For now, let’s position them one below the other using a LinearLayoutManager
instance.
For optimal performance, I suggest you also indicate that the size of the RecyclerView
widget is not going to change during runtime.
Add the following code to your main activity:
my_rv.layoutManager = LinearLayoutManager(this) my_rv.setHasFixedSize(true)
Finally, assign a new instance of your adapter to the adapter
property of the RecyclerView
widget.
val adapter = MyAdapter(myList, this) my_rv.adapter = adapter
If you run your app now, you’ll be able to see the list.
8. Create a Selection Tracker
The RecyclerView
widget still doesn’t allow you to select any items. To enable multi-item selection, you’ll need a SelectionTracker
object in your activity.
private var tracker: SelectionTracker<Long>? = null
You can initialize the tracker using the SelectionTracker.Builder
class. To its constructor, you must pass a selection ID, your RecyclerView
widget, a key provider, your item details lookup class, and a storage strategy.
You are free to use any string as a selection ID. As a key provider, you can use an instance of the StableIdKeyProvider
class.
The RecyclerView Selection library offers a variety of storage strategies, all of which ensure that selected items are not deselected when the user’s device is rotated or when the Android system closes your app during a resource crunch. For now, because the type of your selection keys is Long
, you must use a StorageStrategy
object of type Long
.
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.
Accordingly, add the following code inside your activity’s onCreate()
method:
tracker = SelectionTracker.Builder<Long>( "selection-1", my_rv, StableIdKeyProvider(my_rv), MyLookup(my_rv), StorageStrategy.createLongStorage() ).withSelectionPredicate( SelectionPredicates.createSelectAnything() ).build()
To make the most of the storage strategy, you must always try to restore the state of the tracker inside the onCreate()
method.
if(savedInstanceState != null) tracker?.onRestoreInstanceState(savedInstanceState)
Similarly, you must make sure you save the state of the tracker inside your activity’s onSaveInstanceState()
method.
override fun onSaveInstanceState(outState: Bundle?) { super.onSaveInstanceState(outState) if(outState != null) tracker?.onSaveInstanceState(outState) }
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.
adapter.setTracker(tracker)
The setTracker()
method doesn’t exist yet, so add the following code inside your adapter class:
private var tracker: SelectionTracker<Long>? = null fun setTracker(tracker: SelectionTracker<Long>?) { this.tracker = tracker }
If you try running your app at this point, you will be able to select items in the list. When you enter the multi-item selection mode by long-pressing a list item, you’ll be able to feel a brief vibration on most devices. However, because the selected items are currently indistinguishable from the unselected ones, you’ll have no visual feedback. To fix this, you need to make a few changes inside the onBindViewHolder()
method of your adapter.
The conventional way to highlight selected items is to change their background color. Therefore, you must now change the background color of the LinearLayout
widget that’s present in your items’ layout XML file. To get a reference to it, get a reference to the parent of one of the TextView
widgets available in the view holder.
Add the following code just before the end of the onBindViewHolder()
method:
val parent = vh.name.parent as LinearLayout // More code here
Next, you can call the isSelected()
method of the SelectionTracker
object to determine whether an item is selected or not.
The following code shows you how to change the background color of selected items to cyan:
if(tracker!!.isSelected(position.toLong())) { parent.background = ColorDrawable( Color.parseColor("#80deea") ) } else { // Reset color to white if not selected parent.background = ColorDrawable(Color.WHITE) }
If you run the app now, you should be able to see the items you select.
9. Create a Selection Observer
Usually, you’d want to show the user how many items are currently selected. With the RecyclerView Selection library, doing so is very easy.
Associate a SelectionObserver
object with your selection tracker by calling the addObserver()
method. Inside the onSelectionChanged()
method of the observer, you can detect changes in the number of items selected.
tracker?.addObserver( object: SelectionTracker.SelectionObserver<Long>() { override fun onSelectionChanged() { val nItems:Int? = tracker?.selection?.size() // More code here } })
How you display the number of selected items is up to you. For now, I suggest you display the number directly in the action bar of your activity. Optionally, you can also change the background color of the action bar to let the user know that there is an active selection in the list. The following code shows you how:
if(nItems!=null && nItems > 0) { // Change title and color of action bar title = "$nItems items selected" supportActionBar?.setBackgroundDrawable( ColorDrawable(Color.parseColor("#ef6c00"))) } else { // Reset color and title to default values title = "RVSelection" supportActionBar?.setBackgroundDrawable( ColorDrawable(getColor(R.color.colorPrimary))) }
If you run the app again, you should now see the title change to reflect the number of list items you select.
Conclusion
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 looks of selected items so that users can tell them apart from unselected ones.
To learn more about the library, refer to the official documentation.