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 that display items let users 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 by showing a detail view.

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

What you should already know

What you'll learn

What you'll do

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.

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.

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 that navigates to a fragment that shows details about the clicked 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:

  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).

  3. 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.

  4. In the sleepdetail package, open and inspect the code for the SleepDetailViewModelFactory.

  5. 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.

  6. 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.

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 need to listen to and receive the click and determine which item has been clicked. Then, you need to respond to the click with an action.

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

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

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

  1. In the sleeptracker folder, 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 will set the android:onClick property of the view later to this function.)
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 a clickListener callback in the constructor of SleepNightListener and assign it to onClick().

    Giving the lambda that handles the click a name, clickListener , helps keep track of it as it is passed between classes. 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 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.

    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. 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.

  1. Inside the ViewHolder class, inside the bind() function, assign the click listener to the binding object. You see an error because you need to update the binding object.
binding.clickListener = clickListener
  1. To update data binding, Clean and Rebuild your project. (You may need to invalidate caches as well.) So, 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.

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.

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 SleepTrackerViewModel, towards the end, define 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, onSleepNighClicked(), in the sleepTrackerViewModel when an item is tapped. Pass in the nightId, so the view model knows which sleep night to get. This leaves you with an error, because you haven't defined onSleepNightClicked() yet. You can keep, comment out, or delete the toast, as you wish.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Add the code to observe clicks:

  1. Open SleepTrackerFragment.kt.
  2. In onCreateView(), 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 've done this before in a previous codelab, here is the code:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, 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.

Android Studio project: RecyclerViewClickHandler.

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:

class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
android:onClick="@{() -> clickListener.onClick(sleep)}"
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})

Udacity course:

Android developer documentation:

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:

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.