Android Kotlin Fundamentals: Interacting with RecyclerView items

Stay organized with collections Save and categorize content based on your preferences.

1. Welcome

This codelab is part of the Android Kotlin Fundamentals course. You'll get the most value out of this course if you work through the codelabs in sequence. All the course codelabs are listed on the Android Kotlin Fundamentals codelabs landing page.

Introduction

Most apps that use lists and grids to display items allow users to interact with the items. Tapping an item from a list and seeing the item's details is a very common use case for this type of interaction. To achieve this, you can add click listeners that respond to user taps on items.

In this codelab, you add this type of interaction to your RecyclerView, and build on an extended version of the sleep-tracker app from the previous series of codelabs.

What you should already know

  • Building a basic user interface using an activity, fragments, and views.
  • Navigating between fragments, and using safeArgs to pass data between fragments.
  • View models, view model factories, transformations, and LiveData and their observers.
  • How to create a Room database, create a data access object (DAO), and define entities.
  • How to use coroutines for database and other long-running tasks.
  • How to implement a basic RecyclerView with an Adapter, ViewHolder, and item layout.
  • How to implement data binding for RecyclerView.
  • How to create and use binding adapters to transform data.
  • How to use GridLayoutManager.

What you'll learn

  • How to make items in the RecyclerView clickable. Implement a click listener to navigate to a detail view when an item is clicked.

What you'll do

  • Build on an extended version of the TrackMySleepQuality app from previous codelabs in this series.
  • Add a click listener to your list and start listening for user interaction. When a list item is tapped, it triggers navigation to a fragment with details on the clicked item. The starter code provides code for the detail fragment, as well as the navigation code.

2. App overview

The starting sleep-tracker app has two screens, represented by fragments, as shown in the figure below.

The first screen, shown on the left, has buttons for starting and stopping tracking. The screen shows some of the user's sleep data. The Clear button permanently deletes all the data that the app has collected for the user. The second screen, shown on the right, is for selecting a sleep-quality rating.

This app uses a simplified architecture with a UI controller, view model and LiveData, and a Room database to persist sleep data.

f24b713c78982a7f.png

In this codelab, you add the ability to respond when a user taps an item in the grid, which brings up a detail screen like the one below. The code for this screen (fragment, view model, and navigation) is provided with the starter app, and you will implement the click-handling mechanism.

951cd24cfb446c25.png

3. Task: Get the starter code and inspect the changes to the app

Step 1: Get the starter app

  1. Download the RecyclerViewClickHandler-Starter code from GitHub and open the project in Android Studio.
  2. Build and run the starter sleep-tracker app.

[Optional] Update your app if you want to use the app from the previous codelab

If you are going to work from the starter app provided in GitHub for this codelab, skip to the next step.

If you want to continue using your own sleep-tracker app that you built in the previous codelab, follow the instructions below to update your existing app so that it has the code for the details-screen fragment.

  1. Even if you are continuing on with your existing app, get the RecyclerViewClickHandler-Starter code from GitHub so that you can copy the files.
  2. Copy all the files in the sleepdetail package.
  3. In the layout folder, copy the file fragment_sleep_detail.xml.
  4. Copy the updated contents of navigation.xml, which adds the navigation for the sleep_detail_fragment.
  5. In the database package, in the SleepDatabaseDao, add the new getNightWithId() method:
/**
 * Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>
  1. In res/values/strings add the following string resource:
<string name="close">Close</string>
  1. Clean and rebuild your app to update data binding.

Step 2: Inspect the code for the sleep details screen

In this codelab, you implement a click handler for a sleep night. Once clicked, the app will navigate to a fragment that shows details about that particular sleep night. Your starter code already contains the fragment and navigation graph for this SleepDetailFragment, because it's quite a bit of code (and fragments and navigation are not part of this codelab). Familiarize yourself with the following code details:

  1. In your app, find the sleepdetail package. This package contains the fragment, view model, and view model factory for a fragment that displays details for one night of sleep.
  2. In the sleepdetail package, open and inspect the code for the SleepDetailViewModel. This view model takes the key for a SleepNight and a DAO in the constructor.

The body of the class has code to get the SleepNight for the given key, and the navigateToSleepTracker variable to control navigation back to the SleepTrackerFragment when the Close button is pressed.

The getNightWithId() function returns a LiveData<SleepNight> and is defined in the SleepDatabaseDao (in the database package).

  1. In the sleepdetail package, open and inspect the code for the SleepDetailFragment. Notice the setup for data binding, the view model, and the observer for navigation.
  2. In the sleepdetail package, open and inspect the code for the SleepDetailViewModelFactory.
  3. In the layout folder, inspect fragment_sleep_detail.xml. Notice the sleepDetailViewModel variable defined in the <data> tag to get the data to display in each view from the view model.

The layout contains a ConstraintLayout that contains an ImageView for the sleep quality, a TextView for a quality rating, a TextView for the sleep length, and a Button to close the detail fragment.

  1. Open the navigation.xml file. For the sleep_tracker_fragment, notice the new action for the sleep_detail_fragment.

The new action, action_sleep_tracker_fragment_to_sleepDetailFragment, is the navigation from the sleep tracker fragment to the details screen.

4. Task: Make items clickable

In this task, you update the RecyclerView to respond to user taps by showing a details screen for the tapped item.

Receiving clicks and handling them is a two-part task: First, you listen to and receive the click and determine which item has been clicked. Then, you respond to the click with an action.

So, where is the best place for adding a click listener for this app?

  • The SleepTrackerFragment has a large number of different Views. Listening to click events at this fragment level won't tell you which item was clicked. It won't even tell you whether an item in a RecyclerView was clicked or any other UI element.
  • Listening at the RecyclerView level, it's hard to figure out exactly what item in the list the user clicked on.
  • The best place to get information from one clicked item is in the ViewHolder object, since it represents one list item.

While the ViewHolder is a great place to listen for clicks, it's not usually the right place to handle them. So, where is the best place to handle the clicks?

  • The Adapter displays data items in views, so you could handle clicks in the adapter. However, from an architectural point of view, the adapter's job is to adapt data for display, not deal with app logic.
  • You should usually handle clicks in the ViewModel. The ViewModel has access to the data and logic for determining what needs to happen in response to the click.

Step 1: Create a click listener and trigger it from the item layout

  1. In the sleeptracker package, open SleepNightAdapter.kt.
  2. At the end of the file, at the top level, create a new listener class, SleepNightListener.
class SleepNightListener() {
    
}
  1. Inside the SleepNightListener class, add an onClick() function. When the view that displays a list item is clicked, the view calls this onClick() function. You still will have to set the android:onClick property of the view later in its layout file which you will add later on in this codelab.
class SleepNightListener() {
    fun onClick() = ...
}
  1. Add a function argument night of type SleepNight to onClick(). The view knows what item it is displaying, and that information needs to be passed on for handling the click.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. To define what onClick() does, provide an onClickListener callback argument in the constructor of SleepNightListener and assign it to onClick().

Your callback that handles the click should have a useful identifier name. Use clickListener as its name. The clickListener callback only needs the night.nightId to access data from the database. Your finished SleepNightListener class should look like the code below.

class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. Open res > layout > list_item_sleep_night.xml.
  2. Inside the data block, add a new variable to make the SleepNightListener class available through data binding. Give the new <variable> a name of clickListener. Set the type to the fully qualified name of the class com.example.android.trackmysleepquality.sleeptracker.SleepNightListener, as shown below. You can now access the onClick() function in SleepNightListener from this layout.
<variable
            name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. To listen for clicks on any part of this list item, add the android:onClick attribute to the ConstraintLayout in the list_item_sleep_night.xml layout file..

Set the attribute to clickListener.onClick(sleep) using a data binding lambda, as shown below:

android:onClick="@{() -> clickListener.onClick(sleep)}"

Step 2: Pass the click listener to the view holder and the binding object

  1. Open SleepNightAdapter.kt.
  2. Modify the constructor of the SleepNightAdapter class to receive a val clickListener: SleepNightListener. When the adapter binds the ViewHolder, it will need to provide it with this click listener.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. In onBindViewHolder(), update the call to holder.bind() to also pass the click listener to the ViewHolder. You will get a compiler error because you added a parameter to the function call.
holder.bind(getItem(position)!!, clickListener)
  1. To fix the compiler error, add the clickListener parameter to bind(). To do this, put the cursor on the error, and press Alt+Enter (Windows) or Option+Enter (Mac) on the error to , as shown in the screenshot below.

b3997303d8426434.png

  1. Inside the ViewHolder class, assign the click listener to the binding object inside the bind() function. You see an error because you need to update the binding object.
binding.clickListener = clickListener

You have taken a click listener from the adapter constructor, and passed it all the way to the view holder and into the binding object.

  1. To update data binding, Clean and Rebuild the project. You may need to invalidate caches as well. Android Studio may still indicate a compiler error which will be fixed in the next section.

Step 3: Display a toast when an item is tapped

You now have the code in place to capture a click, but you haven't implemented what happens when a list item is tapped. The simplest response is to display a toast showing the nightId when an item is clicked. This verifies that when a list item is clicked, the correct nightId is captured and passed on.

  1. Open SleepTrackerFragment.kt.
  2. In onCreateView(), find the adapter variable. Notice that it shows an error, because it now expects a click listener parameter.
  3. Define a click listener by passing in a lambda to the SleepNightAdapter. This simple lambda just displays a toast showing the nightId, as shown below. You'll have to import Toast. Below is the complete updated definition.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Run the app, tap items, and verify that they display a toast with the correct nightId. Because items have increasing nightId values, and the app displays the most recent night first, the item with the lowest nightId is at the bottom of the list.

5. Task: Handle item clicks

In this task, you change the behavior when an item in the RecyclerView is clicked, so that instead of showing a toast, the app will navigate to a detail fragment that shows more information about the clicked night.

Step 1: Navigate on click

In this step, instead of just displaying a toast, you change the click listener lambda in onCreateView() of the SleepTrackerFragment to pass the nightId to the SleepTrackerViewModel and trigger navigation to the SleepDetailFragment.

Define the click handler function:

  1. Open SleepTrackerViewModel.kt.
  2. Inside the SleepTrackerViewModel class, towards the end of the class definition, create the onSleepNightClicked()click handler function.
fun onSleepNightClicked(id: Long) {

}
  1. Inside the onSleepNightClicked(), trigger navigation by setting _navigateToSleepDetail to the passed in id of the clicked sleep night.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. Implement _navigateToSleepDetail. As you've done before, define a private MutableLiveData for the navigation state. And a public gettable val to go with it.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Define the method to call after the app has finished navigating. Call it onSleepDetailNavigated() and set its value to null.
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

Add the code to call the click handler:

  1. Open SleepTrackerFragment.kt and scroll down to the code that creates the adapter and defines SleepNightListener to show a toast.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Add the following code below the toast to call a click handler, onSleepNightClicked(), in the sleepTrackerViewModel when an item is tapped. Pass in the nightId, so the view model knows which sleep night to get.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Add the code to observe clicks:

  1. Open SleepTrackerFragment.kt.
  2. In onCreateView(), at the bottom of the function, right above the declaration of manager, add code to observe the new navigateToSleepDetail LiveData. When navigateToSleepDetail changes, navigate to the SleepDetailFragment, passing in the night, then call onSleepDetailNavigated() afterwards. Since you have done this before in a previous codelab, here is the code:
sleepTrackerViewModel.navigateToSleepDetail.observe(viewLifecycleOwner, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. Run your code, click on an item, and ... the app crashes.

Handle null values in the binding adapters:

  1. Run the app again, in debug mode. Tap an item, and filter the logs to show Errors. It will show a stack trace including something like what's below.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

Unfortunately, the stack trace does not make it obvious where this error is triggered. One disadvantage of data binding is that it can make it harder to debug your code. The app crashes when you click an item, and the only new code is for handling the click.

However, it turns out that with this new click-handling mechanism, it is now possible for the binding adapters to get called with a null value for item. In particular, when the app starts, the LiveData starts as null, so you need to add null checks to each of the adapters.

  1. In BindingUtils.kt, for each of the binding adapters, change the type of the item argument to nullable, and wrap the body with item?.let{...}. For example, your adapter for sleepQualityString will look like this. Change the other adapters likewise.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Run your app. Tap on an item, and a detail view opens.

6. Solution code

Android Studio project: RecyclerViewClickHandler.

7. Summary

To make items in a RecyclerView respond to clicks, attach click listeners to list items in the ViewHolder, and handle clicks in the ViewModel.

To make items in a RecyclerView respond to clicks, you need to do the following:

  • Create a listener class that takes a lambda and assigns it to an onClick() function.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • Set the click listener on the view.
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • Pass the click listener to the adapter constructor, into the view holder, and add it to the binding object.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
  • In the fragment that shows the recycler view, where you create the adapter, define a click listener by passing a lambda to the adapter.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • Implement the click handler in the view model. For clicks on list items, this commonly triggers navigation to a detail fragment.

8. Learn more

Udacity course:

Android developer documentation:

9. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

Assume that your app contains a RecyclerView that displays items in a shopping list. Your app also defines a click-listener class:

class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {
    fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}

How do you make the ShoppingListItemListener available to data binding? Select one.

▢ In the layout file that contains the RecyclerView that displays the shopping list, add a <data> variable for ShoppingListItemListener.

▢ In the layout file that defines the layout for a single row in the shopping list, add a <data> variable for ShoppingListItemListener.

▢ In the ShoppingListItemListener class, add a function to enable data binding:

fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}

▢ In the ShoppingListItemListener class, inside the onClick() function, add a call to enable data binding:

fun onClick(cartItem: CartItem) = { 
    clickListener(cartItem.itemId)
    dataBindingEnable(true)
}

Question 2

Where do you add the android:onClick attribute to make items in a RecyclerView respond to clicks? Select all that apply.

▢ In the layout file that displays the RecyclerView, add it to <androidx.recyclerview.widget.RecyclerView>

▢ Add it to the layout file for an item in the row. If you want the entire item to be clickable, add it to the parent view that contains the items in the row.

▢ Add it to the layout file for an item in the row. If you want a single TextView in the item to be clickable, add it to the <TextView>.

▢ Always add it the layout file for the MainActivity.

10. Next codelab

Start the next lesson: