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 anAdapter
,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.
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.
3. Task: Get the starter code and inspect the changes to the app
Step 1: Get the starter app
- Download the RecyclerViewClickHandler-Starter code from GitHub and open the project in Android Studio.
- 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.
- Even if you are continuing on with your existing app, get the RecyclerViewClickHandler-Starter code from GitHub so that you can copy the files.
- Copy all the files in the
sleepdetail
package. - In the
layout
folder, copy the filefragment_sleep_detail.xml
. - Copy the updated contents of
navigation.xml
, which adds the navigation for thesleep_detail_fragment
. - In the
database
package, in theSleepDatabaseDao
, add the newgetNightWithId()
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>
- In
res/values/strings
add the following string resource:
<string name="close">Close</string>
- 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:
- 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. - In the
sleepdetail
package, open and inspect the code for theSleepDetailViewModel
. This view model takes the key for aSleepNight
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).
- In the
sleepdetail
package, open and inspect the code for theSleepDetailFragment
. Notice the setup for data binding, the view model, and the observer for navigation. - In the
sleepdetail
package, open and inspect the code for theSleepDetailViewModelFactory
. - In the layout folder, inspect
fragment_sleep_detail.xml
. Notice thesleepDetailViewModel
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.
- Open the
navigation.xml
file. For thesleep_tracker_fragment
, notice the new action for thesleep_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 differentViews
. 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 aRecyclerView
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.
TheViewModel
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
- In the
sleeptracker
package, open SleepNightAdapter.kt. - At the end of the file, at the top level, create a new listener class,
SleepNightListener
.
class SleepNightListener() {
}
- Inside the
SleepNightListener
class, add anonClick()
function. When the view that displays a list item is clicked, the view calls thisonClick()
function. You still will have to set theandroid:onClick
property of the view later in its layout file which you will add later on in this codelab.
class SleepNightListener() {
fun onClick() = ...
}
- Add a function argument
night
of typeSleepNight
toonClick()
. 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) =
}
- To define what
onClick()
does, provide anonClickListener
callback argument in the constructor ofSleepNightListener
and assign it toonClick()
.
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)
}
- Open res > layout > list_item_sleep_night.xml.
- Inside the
data
block, add a new variable to make theSleepNightListener
class available through data binding. Give the new<variable>
aname
ofclickListener.
Set thetype
to the fully qualified name of the classcom.example.android.trackmysleepquality.sleeptracker.SleepNightListener
, as shown below. You can now access theonClick()
function inSleepNightListener
from this layout.
<variable
name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
- To listen for clicks on any part of this list item, add the
android:onClick
attribute to theConstraintLayout
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
- Open SleepNightAdapter.kt.
- Modify the constructor of the
SleepNightAdapter
class to receive aval clickListener: SleepNightListener
. When the adapter binds theViewHolder
, it will need to provide it with this click listener.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
- In
onBindViewHolder()
, update the call toholder.bind()
to also pass the click listener to theViewHolder
. You will get a compiler error because you added a parameter to the function call.
holder.bind(getItem(position)!!, clickListener)
- To fix the compiler error, add the
clickListener
parameter tobind()
. To do this, put the cursor on the error, and pressAlt+Enter
(Windows) orOption+Enter
(Mac) on the error to , as shown in the screenshot below.
- Inside the
ViewHolder
class, assign the click listener to thebinding
object inside thebind()
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.
- 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.
- Open SleepTrackerFragment.kt.
- In
onCreateView()
, find theadapter
variable. Notice that it shows an error, because it now expects a click listener parameter. - Define a click listener by passing in a lambda to the
SleepNightAdapter
. This simple lambda just displays a toast showing thenightId
, as shown below. You'll have to importToast
. Below is the complete updated definition.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
- Run the app, tap items, and verify that they display a toast with the correct
nightId
. Because items have increasingnightId
values, and the app displays the most recent night first, the item with the lowestnightId
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:
- Open SleepTrackerViewModel.kt.
- Inside the
SleepTrackerViewModel
class, towards the end of the class definition, create theonSleepNightClicked()
click handler function.
fun onSleepNightClicked(id: Long) {
}
- Inside the
onSleepNightClicked()
, trigger navigation by setting_navigateToSleepDetail
to the passed inid
of the clicked sleep night.
fun onSleepNightClicked(id: Long) {
_navigateToSleepDetail.value = id
}
- Implement
_navigateToSleepDetail
. As you've done before, define aprivate MutableLiveData
for the navigation state. And a public gettableval
to go with it.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
get() = _navigateToSleepDetail
- Define the method to call after the app has finished navigating. Call it
onSleepDetailNavigated()
and set its value tonull
.
fun onSleepDetailNavigated() {
_navigateToSleepDetail.value = null
}
Add the code to call the click handler:
- 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()
})
- Add the following code below the toast to call a click handler,
onSleepNightClicked()
, in thesleepTrackerViewModel
when an item is tapped. Pass in thenightId
, so the view model knows which sleep night to get.
sleepTrackerViewModel.onSleepNightClicked(nightId)
Add the code to observe clicks:
- Open SleepTrackerFragment.kt.
- In
onCreateView()
, at the bottom of the function, right above the declaration ofmanager
, add code to observe the newnavigateToSleepDetail
LiveData
. WhennavigateToSleepDetail
changes, navigate to theSleepDetailFragment
, passing in thenight
, then callonSleepDetailNavigated()
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()
}
})
- Run your code, click on an item, and ... the app crashes.
Handle null values in the binding adapters:
- 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.
- In
BindingUtils.kt
, for each of the binding adapters, change the type of theitem
argument to nullable, and wrap the body withitem?.let{...}
. For example, your adapter forsleepQualityString
will look like this. Change the other adapters likewise.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
item?.let {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
}
- 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
.