Lesson 07: RecyclerView
Everything is better in a list! Recycler View has been - and continues to be - an essential component of any app design. This lesson is all about making your app better with Recycler Views.
RecyclerView
Displaying a list or grid of data is one of the most common UI tasks in Android. Lists vary from simple to very complex. A list of text views might show simple data, such as a shopping list. A complex list, such as an annotated list of vacation destinations, might show the user many details inside a scrolling grid with headers.
To support all these use cases, Android provides the RecyclerView
widget.
-
RecyclerView
is designed to be efficient even when displaying extremely large lists. It allows you to build everything from simple lists ofTextViews
all the way to very complex collections of views. -
It only support displaying different types of items in the same list. For example, a news app may want to mix video items into a list of headlines.
-
RecyclerView
is also highly customizable. Out-of-the-box, it supports lists and grids. You can configure it to scroll horizontally or vertically. If the default options aren’t enough, you can even build your own layout manager to makeRecyclerView
display any design you dream up.
Advantages of RecyclerView
-
Efficient display of large list.
-
Minimizing refreshes when an item is updated, deleted, or added to the list.
-
Reusing views that scroll off screen to display the next item that scrolls on screen.
-
Displaying items in a list or a grid.
-
Scrolling vertically or horizontally.
-
Allowing custom layouts when a list or a grid is not enough for the use case.
The greatest benefit of RecyclerView
is that it is very efficient for large lists:
-
By default,
RecyclerView
only does work to process or draw items that are currently visible on the screen. For example, if your list has a thousand elements but only 10 elements are visible,RecyclerView
does only enough work to draw 10 items on the screen. When the user scrolls,RecyclerView
figures out what new items should be on the screen and does just enough work to display those items. -
When an item scrolls off the screen, the item’s views are recycled. That means the item is filled with new content that scrolls onto the screen. This
RecyclerView
behavior saves a lot of processing time and helps lists scroll fluidly. -
When an item changes, instead of redrawing the entire list,
RecyclerView
can update that one item. This is a huge efficiency gain when displaying lists of complex items!
In the sequence shown below, you can see that one view has been filled with data, ABC
. After that view scrolls off the screen, RecyclerView
reuses the view for new data, XYZ
.
Other options
RecyclerView
, while efficient and customizable, is not the only way to display a list of things on Androids.
Android also ships with a few other options for displaying lists.
-
The first are
ListView
andGridView
for displaying a scrolling list and grid respectively. You can think of this asRecyclerView
simpler, but less powerful siblings. Both of them work for displaying a small list of items that aren’t too complex, like 100 items. They are not nearly as efficient asRecyclerView
, and they don’t offer nearly as many options for customizing the display. -
The other option is
LinearLayout
. You’ve already seen that earlier linear layout can be used to display a small list of items, for example three to five.
Your first RecyclerView
The adapter pattern
If you ever travel between countries that use different electric sockets, you probably know how you can plug your devices into outlets by using an adapter. The adapter lets you convert one type of plug to another, which is really converting one interface into another.
The adapter pattern in software engineering helps an object to work with another API. RecyclerView
uses an adapter to transform app data into something the RecyclerView
can display, without changing how the app stores and processes the data. For the sleep-tracker app, you build an adapter that adapts data from the Room
database into something that RecyclerView
knows how to display, without changing the ViewModel
.
Adapter Interface
RecyclerView
adapters must provide a few methods for RecyclerView
to understand how to display the data on screen.
-
First, in order to know how far to scroll, the adapter needs to tell the recycler view how many items are available.
-
Then you provide a way for the RecyclerView to draw a specific item.
RecyclerView
we’ll use this anytime an item enters the screen or if you update or add an item that’s churned on the screen. -
Finally, you must provide
RecyclerView
with a way to create a new view for an item. This is important because while recycler view is responsible for handling the recycling and efficient display of the views, it has no idea what kind of views you would like to display.
How does RecyclerView works?
-
When
RecyclerView
runs, it will use the adapter to figure out how to display your data on screen. -
When it first starts out, it will ask the adapter how many items there are. If the adapter set there is 100, it will immediately start creating just the views needed for the first screen.
-
First, recycler view will ask the adapter to create a new view for the first data item in your list.
-
Once it has the view, it will ask the adapter to draw the item. It will repeat this until it doesn’t need any more views to fill the screen.
-
Then your
RecyclerView
is done. It won’t look at the other items in the list until the user scrolls to list on screen. -
If it’s items go off the screen, they will be reused or recycled in the next position that gets displayed.
-
When recycling,
RecyclerView
doesn’t need to create a view. It will just use the old one and ask the adapter how to draw the next item into it.
ViewHolder
To implement recycling and support multiple types of views, RecyclerView
doesn’t interact it to views but instead ViewHolders
. This is the last part of the adapter interface.
-
ViewHolders
do exactly what it sounds like they do, hold views. They also had a lot of extra information thatRecyclerView
uses to efficiently move views around the screen. -
ViewsHolders
know things like the last position the items have in the list, which is important when you are animating list changes. In other words, storing information for RecyclerView. -
ViewHolders
really don’t do that much, and they’re mostly an implementation detail of theRecyclerView
. Your adapter will take care of providing anyViewHolders
that theRecyclerView
needs.
Implementing a RecyclerView
To display your data in a RecyclerView
, you need the following parts:
-
Data to display.
-
A
RecyclerView
instance defined in your layout file, to act as the container for the views. -
A layout for one item of data.
If all the list items look the same, you can use the same layout for all of them, but that is not mandatory. The item layout has to be created separately from the fragment’s layout, so that one item view at a time can be created and filled with data. -
A layout manager.
ARecyclerView
uses aLayoutManager
to organize the layout of the items in theRecyclerView
, such as laying them out in a grid or in a linear list. In the<RecyclerView>
in the layout file, set theapp:layoutManager
attribute to the layout manager (such asLinearLayoutManager
orGridLayoutManager
). You can also set theLayoutManager
for aRecyclerView
programmatically. -
A view holder.
The view holder extends theViewHolder
class. It contains the view information for displaying one item from the item’s layout. View holders also add information thatRecyclerView
uses to efficiently move views around the screen. -
The
onBindViewHolder()
method in the adapter adapts the data to the views. You always override this method. Typically,onBindViewHolder()
inflates the layout for an item, and puts the data in the views in the layout. -
Because the
RecyclerView
knows nothing about the data, theAdapter
needs to inform theRecyclerView
when that data changes. UsenotifyDataSetChanged()
to notify theAdapter
that the data has changed. -
An adapter.
The adapter connects your data to theRecyclerView
. It adapts the data so that it can be displayed in aViewHolder
. ARecyclerView
uses the adapter to figure out how to display the data on the screen.
The adapter requires you to implement the following methods:
–getItemCount()
to return the number of items.
–onCreateViewHolder()
to return theViewHolder
for an item in the list.
–onBindViewHolder()
to adapt the data to the views for an item in the list.
Adding a RecyclerView
Step 1: Add RecyclerView
with LayoutManager
- Open the module
build.gradle
file, scroll to the end, add a new dependency.
- Open the
fragment_sleep_tracker.xml
layout file in the Code tab in Android Studio and replace the entireScrollView
, including the enclosedTextView
, with aRecyclerView
. - Add a layout manager to the
RecyclerView
XML. EveryRecyclerView
needs a layout manager that tells it how to position items in the list. Android provides aLinearLayoutManager
, which by default lays out the items in a vertical list of full width rows.
Step 2: Create the list item layout and text view holder
The RecyclerView
is only a container. In this step, you create the layout and infrastructure for the items to be displayed inside the RecyclerView
.
To get to a working RecyclerView
as quickly as possible, at first you use a simplistic list item that only displays the sleep quality as a number. For this, you need a view holder, TextItemViewHolder
. You also need a view, a TextView
, for the data. (In a later step, you learn more about view holders and how to lay out all the sleep data.)
- Create a layout file called
text_item_view.xml
. It doesn’t matter what you use as the root element, because you’ll replace the template code. - In
text_item_view.xml
, delete all the given code. - Add a
TextView
with16dp
padding at the start and end, and a text size of24sp
. Let the width match the parent, and the height wrap the content. Because this view is displayed inside theRecyclerView
, you don’t have to place the view inside aViewGroup
.
- Open
Util.kt
. Scroll to the end and add the definition that’s shown below, which creates theTextItemViewHolder
class. Put the code at the bottom of the file, after the last closing brace. The code goes inUtil.kt
because this view holder is temporary, and you replace it later.
- If you are prompted, import
android.widget.TextView
andandroidx.recyclerview.widget.RecyclerView
.
Step 3: Create SleepNightAdapter
The core task in implementing a RecyclerView
is creating the adapter. You have a simple view holder for the item view, and a layout for each item. You can now create an adapter. The adapter creates a view holder and fills it with data for the RecyclerView
to display.
- In the
sleeptracker
package, create a new Kotlin class calledSleepNightAdapter
. - Make the
SleepNightAdapter
class extendRecyclerView.Adapter
. The class is calledSleepNightAdapter
because it adapts aSleepNight
object into something thatRecyclerView
can use. The adapter needs to know what view holder to use, so pass inTextItemViewHolder
. Import necessary components when prompted, and then you’ll see an error, because there are mandatory methods to implement.
- At the top level of
SleepNightAdapter
, create alistOf
SleepNight
a data source variable to hold the data.
- In
SleepNightAdapter
, overridegetItemCount()
to return the size of the list of sleep nights indata
. TheRecyclerView
needs to know how many items the adapter has for it to display, and it does that by callinggetItemCount()
.
- In
SleepNightAdapter
, override theonBindViewHolder()
function, as shown below. TheonBindViewHolder()
function is called byRecyclerView
to display the data for one list item at the specified position. So theonBindViewHolder()
method takes two arguments: a view holder, and a position of the data to bind. For this app, the holder is theTextItemViewHolder
, and the position is the position in the list.
- Inside
onBindViewHolder()
, create a variable for one item at a given position in the data.
- The
ViewHolder
you created has a property calledtextView
. InsideonBindViewHolder()
, set thetext
of thetextView
to the sleep-quality number. This code displays only a list of numbers, but this simple example lets you see how the adapter gets the data into the view holder and onto the screen.
- In
SleepNightAdapter
, override and implementonCreateViewHolder()
, which is called when theRecyclerView
needs a view holder to represent an item. This function takes two parameters and returns aViewHolder
. Theparent
parameter, which is the view group that holds the view holder, is always theRecyclerView
. TheviewType
parameter is used when there are multiple views in the sameRecyclerView
. For example, if you put a list of text views, an image, and a video all in the sameRecyclerView
, theonCreateViewHolder()
function would need to know what type of view to use.
- In
onCreateViewHolder()
, create an instance ofLayoutInflater
. The layout inflater knows how to create views from XML layouts. Thecontext
contains information on how to correctly inflate the view. In an adapter for a recycler view, you always pass in the context of theparent
view group, which is theRecyclerView
.
- In
onCreateViewHolder()
, create theview
by asking thelayoutinflater
to inflate it. Pass in the XML layout for the view, and theparent
view group for the view. The third, boolean, argument isattachToRoot
. This argument needs to befalse
, becauseRecyclerView
adds this item to the view hierarchy for you when it’s time.
- In
onCreateViewHolder()
, return aTextItemViewHolder
made withview
.
- The adapter needs to let the
RecyclerView
know when thedata
has changed, because theRecyclerView
knows nothing about the data. It only knows about the view holders that the adapter gives to it. To tell theRecyclerView
when the data that it’s displaying has changed, add a custom setter to thedata
variable that’s at the top of theSleepNightAdapter
class. In the setter, givedata
a new value, then callnotifyDataSetChanged()
to trigger redrawing the list with the new data.
Note: When notifyDataSetChanged()
is called, the RecyclerView
redraws the whole list, not just the changed items. This is simple, and it works for now. You improve on this code later.
SleepNightAdapter
full code should be like:
Step 4: Tell RecyclerView about the Adapter
The RecyclerView
needs to know about the adapter to use to get view holders.
- Open
SleepTrackerFragment.kt
. InonCreateview()
, create an adapter. Put this code after the creation of theViewModel
model, and before thereturn
statement.
- Associate the
adapter
with theRecyclerView
using binding.
- Clean and rebuild your project to update the
binding
object. If you still see errors aroundbinding.sleepList
orbinding.FragmentSleepTrackerBinding
, invalidate caches and restart. (Select File > Invalidate Caches / Restart.)
Step 5: Get data into the adapter
So far you have an adapter, and a way to get data from the adapter into the RecyclerView
. Now you need to get data into the adapter from the ViewModel
.
- Open
SleepTrackerViewModel
. Find thenights
variable, which stores all the sleep nights, which is the data to display. Thenights
variable is set by callinggetAllNights()
on the database. Removeprivate
fromnights
, because you will create an observer that needs to access this variable. Your declaration should look like this:
-
In the
database
package, open theSleepDatabaseDao
. Find thegetAllNights()
function. Notice that this function returns a list ofSleepNight
values asLiveData
. This means that thenights
variable containsLiveData
that is kept updated byRoom
, and you can observenights
to know when it changes. -
Open
SleepTrackerFragment
. InonCreateView()
, below the creation of theadapter
, create an observer on thenights
variable. By supplying the fragment’sviewLifecycleOwner
as the lifecycle owner, you can make sure this observer is only active when theRecyclerView
is on the screen.
- Inside the observer, whenever you get a non-null value (for
nights
), assign the value to the adapter’sdata
. This is the completed code for the observer and setting the data:
Debugging tips
If your app compiles but doesn’t work, here are a few things to check:
-
Make sure you’ve added at least one night of sleep.
-
Do you call
notifyDataSetChanged()
inSleepNightAdapter
? -
Try setting a breakpoint to make sure it’s getting called.
-
Did you register an observer on
sleepTrackerViewModel.nights
inSleepTrackerFragment
? -
Did you set the adapter in
SleepTrackerFragment
withbinding.sleepList.adapter = adapter
? -
Does
data
inSleepNightAdapter
hold a non-empty list? -
Try setting a breakpoint in the setter and
getItemCount()
.
Recycling ViewHolders
RecyclerView
recycles view holders, which means that it reuses them. As a view scrolls off the screen, RecyclerView
reuses the view for the view that’s about to scroll onto the screen.
Because these view holders are recycled, make sure onBindViewHolder()
sets or resets any customizations that previous items might have set on a view holder.
For example, you could set the text color to red in view holders that hold quality ratings that are less than or equal to 1 and represent poor sleep.
- In the
SleepNightAdapter
class, add the following code to at the end ofonBindViewHolder()
.
-
Run the app. Add some low sleep-quality data, and the number is red. Add high ratings for sleep quality until you see a red high number on the screen. As
RecyclerView
reuses view holders, it eventually reuses one of the red view holders for a high quality rating. The high rating is erroneously displayed in red. -
To fix this, add an
else
statement to set the color to black if the quality is not less than or equal to one. With both conditions explicit, the view holder will use the correct text color for each item.
ViewHolders
Before we dive into making our own ViewHolder
, let’s take a look at the starter ViewHolder
that was provided for this lesson.
- If you open
Utill.kt
, you will find the definition ofTextItemViewHolder
. Taking a look at this declaration, it doesn’t look like it’s doing very much. It’s basic directing aTextView
in aTextItemViewHolder
by declaring it as a wall, it’s available as a property.
- So why does
RecyclerView
not just use aTextView
directly? This one line of code provides a lot of functionality. AViewHolder
describes an item view and metadata about its place within theRecyclerView
.RecyclerView
relies on this functionality to correctly position the view as the list scrolls, and to do interesting things like animate views when items are added or removed in theAdapter
.
A ViewHolder describes an item view and metadata about its place within the RecyclerView
. In other words, a ViewHolder
tells a RecyclerView
where and how an item should get drawn in the list.
itemView is the reference that RecyclerView
reviews when it needs to access the actual view that’s being displayed.
-
RecyclerView
will useitemView
when binding an item to display on the screen, when drawing decorations around the view like a border, and for accessibility. -
RecyclerView
doesn’t care what kind of view is startingitemView
. You can put anything you want here, like aTextView
or even a constraint layout.
getAdapterPosition() can be used by the RecyclerView
to figure out the position in the list that was bound to a particular ViewHolder
.
getLayoutPosition() can used to know in what position the ViewHolder
was displayed.
Most of these methods are final. RecyclerView
will provide them for us and we won’t ever need to coat them. However, there is one method that you might provide. Your ViewHolder
can tell RecyclerView
what its ID is. An ID is just a unique identifier like night ID on our sleep night. When you override getItemId(), RecyclerView
can use this ID when performing animations.
Display the SleepQuality
List
In this step, you create the layout file for one item. The layout consists of a ConstraintLayout
with an ImageView
for the sleep quality, a TextView
for the sleep length, and a TextView
for the quality as text. Because you’ve done layouts before, copy and paste the provided XML code.
Step 1: Create the item layout
- Create a new layout resource file and name it
list_item_sleep_night
. - Replace all the code in the file with the code below. Then familiarize yourself with the layout you just created.
Step 2: Create ViewHolder
- Open
SleepNightAdapter.kt
. Make a class inside theSleepNightAdapter
calledViewHolder
and make it extendRecyclerView.ViewHolder
.
- Inside
ViewHolder
, get references to the views. You need a reference to the views that thisViewHolder
will update. Every time you bind thisViewHolder
, you need to access the image and both text views. (You convert this code to use data binding later.)
Step 3: Use the ViewHolder
in SleepNightAdapter
- In the
SleepNightAdapter
definition, instead ofTextItemViewHolder
, use theSleepNightAdapter.ViewHolder
that you just created.
- Update
onCreateViewHolder()
:
-
Change the signature of
onCreateViewHolder()
to return theViewHolder
. -
Change the layout inflator to use the correct layout resource,
list_item_sleep_night
. -
Remove the cast to
TextView
. -
Instead of returning a
TextItemViewHolder
, return aViewHolder
.
Here is the finished updatedonCreateViewHolder()
function:
- Update
onBindViewHolder()
:
-
Change the signature of
onBindViewHolder()
so that theholder
parameter is aViewHolder
instead of aTextItemViewHolder
. -
Inside
onBindViewHolder()
, delete all the code, except for the definition ofitem
. -
Define a
val
res
that holds a reference to theresources
for this view.
- Set the text of the
sleepLength
text view to the duration. Copy the code below, which calls a formatting function that’s provided with the starter code.
-
This gives an error, because
convertDurationToFormatted()
needs to be defined. OpenUtil.kt
and uncomment the code and associated imports for it. (Select Code > Comment with Line comments.) -
Back in
onBindViewHolder()
, useconvertNumericQualityToString()
to set the quality.
- You may need to manually import these functions.
- Set the correct icon for the quality. The new
ic_sleep_active
icon is provided for you in the starter code.
Here is the finished updated onBindViewHolder()
function, setting all the data for the ViewHolder
:
Refactor onBindViewHolder
Your RecyclerView
is now complete! You learned how to implement an Adapter
and a ViewHolder
, and you put them together to display a list with a RecyclerView
Adapter
.
Your code so far shows the process of creating an adapter and view holder. However, you can improve this code. The code to display and the code to manage view holders is mixed up, and onBindViewHolder()
knows details about how to update the ViewHolder
.
In a production app, you might have multiple view holders, more complex adapters, and multiple developers making changes. You should structure your code so that everything related to a view holder is only in the view holder.
Step 1: Refactor onBindViewHolder()
In this step, you refactor the code and move all the view holder functionality into the ViewHolder
. The purpose of this refactoring is not to change how the app looks to the user, but make it easier and safer for developers to work on the code. Fortunately, Android Studio has tools to help.
- In
SleepNightAdapter
, inonBindViewHolder()
, select everything except the statement to declare the variableitem
. - Right-click, then select Refactor > Extract > Function.
- Name the function
bind
and accept the suggested parameters. Click OK. Thebind()
function is placed belowonBindViewHolder()
.
- Put the cursor on the word
holder
of theholder
parameter ofbind()
. PressAlt+Enter
(Option+Enter
on a Mac) to open the intention menu. Select Convert parameter to receiver to convert this to an extension function that has the following signature:
- Cut and paste the
bind()
function into theViewHolder
. - Make
bind()
public. - Import
bind()
into the adapter, if necessary. - Because it’s now in the
ViewHolder
, you can remove theViewHolder
part of the signature. Here is the final code for thebind()
function in theViewHolder
class.
Step 2: Refactor onCreateViewHolder
The onCreateViewHolder()
method in the adapter currently inflates the view from the layout resource for the ViewHolder
. However, inflation has nothing to do with the adapter, and everything to do with the ViewHolder
. Inflation should happen in the ViewHolder
.
- In
onCreateViewHolder()
, select all the code in the body of the function. Right-click, then select Refactor > Extract > Function. Name the functionfrom
and accept the suggested parameters. Click OK. - Put the cursor on the function name
from
. PressAlt+Enter
(Option+Enter
on a Mac) to open the intention menu. Select Move to companion object. Thefrom()
function needs to be in a companion object so it can be called on theViewHolder
class, not called on aViewHolder
instance. - Move the
companion
object into theViewHolder
class. - Make
from()
public. - In
onCreateViewHolder()
, change thereturn
statement to return the result of callingfrom()
in theViewHolder
class.
Your completed onCreateViewHolder()
and from()
methods should look like the code below, and your code should build and run without errors.
- Change the signature of the
ViewHolder
class so that the constructor is private. Becausefrom()
is now a method that returns a newViewHolder
instance, there’s no reason for anyone to call the constructor ofViewHolder
anymore.
Improving Data Refresh
Here is a recap of using RecyclerView
with the adapter pattern to display sleep data to the user.
-
From user input, the app creates a list of
SleepNight
objects. EachSleepNight
object represents a single night of sleep, its duration, and quality. -
The
SleepNightAdapter
adapts the list ofSleepNight
objects into somethingRecyclerView
can use and display. -
The
SleepNightAdapter
adapter producesViewHolders
that contain the views, data, and meta information for the recycler view to display the data. -
RecyclerView
uses theSleepNightAdapter
to determine how many items there are to display (getItemCount()
).RecyclerView
usesonCreateViewHolder()
andonBindViewHolder()
to get view holders bound to data for displaying.
The notifyDataSetChanged() method is inefficient
- To tell
RecyclerView
that an item in the list has changed and needs to be updated, the current code callsnotifyDataSetChanged()
in theSleepNightAdapter
, as shown below.
-
However,
notifyDataSetChanged()
tellsRecyclerView
that the entire list is potentially invalid. As a result,RecyclerView
rebinds and redraws every item in the list, including items that are not visible on screen. This is a lot of unnecessary work. For large or complex lists, this process could take long enough that the display flickers or stutters as the user scrolls through the list. -
To fix this problem, you can tell
RecyclerView
exactly what has changed.RecyclerView
can then update only the views that changed on screen. -
RecyclerView
has a rich API for updating a single element. You could usenotifyItemChanged()
to tellRecyclerView
that an item has changed, and you could use similar functions for items that are added, removed, or moved. You could do it all manually, but that task would be non-trivial and might involve quite a bit of code. Fortunately, there’s a better way.
DiffUtil: an efficient way that does the hard work for you
RecyclerView
has a class called DiffUtil
which is for calculating the differences between two lists. DiffUtil
takes an old list and a new list and figures out what’s different. It finds items that were added, removed, or changed. Then it uses an algorithm called a Eugene W. Myers’s difference algorithm to figure out the minimum number of changes to make from the old list to produce the new list.
Once DiffUtil
figures out what has changed, RecyclerView
can use that information to update only the items that were changed, added, removed, or moved, which is much more efficient than redoing the entire list.
Refresh Data with DiffUtil
Step 1: Implement SleepNightDiffCallback
In order to use the functionality of the DiffUtil
class, extend DiffUtil.ItemCallback
.
- Open
SleepNightAdapter.kt
. - Below the full class definition for
SleepNightAdapter
, make a new top-level class calledSleepNightDiffCallback
that extendsDiffUtil.ItemCallback
. PassSleepNight
as a generic parameter.
- Put the cursor in the
SleepNightDiffCallback
class name. PressAlt+Enter
(Option+Enter
on Mac) and select Implement Members. In the dialog that opens, shift-left-click to select theareItemsTheSame()
andareContentsTheSame()
methods, then click OK.
This generates stubs insideSleepNightDiffCallback
for the two methods, as shown below.DiffUtil
uses these two methods to figure out how the list and items have changed.
- Inside
areItemsTheSame()
, replace theTODO
with code that tests whether the two passed-inSleepNight
items,oldItem
andnewItem
, are the same. If the items have the samenightId
, they are the same item, so returntrue
. Otherwise, returnfalse
.DiffUtil
uses this test to help discover if an item was added, removed, or moved.
- Inside
areContentsTheSame()
, check whetheroldItem
andnewItem
contain the same data; that is, whether they are equal. This equality check will check all the fields, becauseSleepNight
is a data class.Data
classes automatically defineequals
and a few other methods for you. If there are differences betweenoldItem
andnewItem
, this code tellsDiffUtil
that the item has been updated.
The full code of SleepNightDiffCallback
should be like:
Step 2: Change your adapter to extend ListAdapter
It’s a common pattern to use a RecyclerView
to display a list that changes. RecyclerView
provides an adapter class, ListAdapter
, that helps you build a RecyclerView
adapter that’s backed by a list.
ListAdapter
keeps track of the list for you and notifies the adapter when the list is updated.
- In the
SleepNightAdapter.kt
file, change the class signature ofSleepNightAdapter
to extendListAdapter
. If prompted, importandroidx.recyclerview.widget.ListAdapter
. - Add
SleepNight
as the first argument to theListAdapter
, beforeSleepNightAdapter.ViewHolder
. - Add
SleepNightDiffCallback()
as a parameter to the constructor. TheListAdapter
will use this to figure out what changed in the list. Your finishedSleepNightAdapter
class signature should look as shown below.
- Inside the
SleepNightAdapter
class, delete thedata
field, including the setter. You don’t need it anymore, becauseListAdapter
keeps track of the list for you. - Delete the override of
getItemCount()
, because theListAdapter
implements this method for you. - To get rid of the error in
onBindViewHolder()
, change theitem
variable. Instead of usingdata
to get anitem
, call thegetItem(position)
method that theListAdapter
provides.
Your code needs to tell the ListAdapter
when a changed list is available. ListAdapter
provides a method called submitList()
to tell ListAdapter
that a new version of the list is available. When this method is called, the ListAdapter
diffs the new list against the old one and detects items that were added, removed, moved, or changed. Then the ListAdapter
updates the items shown by RecyclerView
.
- Open
SleepTrackerFragment.kt
. - In
onCreateView()
, in the observer onsleepTrackerViewModel
, find the error where thedata
variable that you’ve deleted is referenced. - Replace
adapter.data = it
with a call toadapter.submitList(it)
. The updated code is shown below.
Using DataBinding
with RecyclerView
With data binding, we can easily observe and show the data to the UI from a data source.
Step 1: Add data binding to the layout file
- Open the
list_item_sleep_night.xml
layout file in the Text tab. - Put the cursor on the
ConstraintLayout
tag and pressAlt+Enter
(Option+Enter
on a Mac). The intention menu (the “quick fix” menu) opens. - Select Convert to data binding layout. This wraps the layout into
<layout>
and adds a<data>
tag inside. - Scroll back to the top, if necessary, and inside the
<data>
tag, declare a variable namedsleep
. - Make its
type
the fully qualified name ofSleepNight
,com.example.android.trackmysleepquality.database.SleepNight
. Your finished<data>
tag should look as shown below.
- To force the creation of the
Binding
object, select Build > Clean Project, then select Build > Rebuild Project. (If you still have problems, select File > Invalidate Caches / Restart.) TheListItemSleepNightBinding
binding object, along with related code, is added to the project’s generated files.
Step 2: Inflate the item layout using data binding
- Open
SleepNightAdapter.kt
. In theViewHolder
class, find theonCreateViewHolder()
method. Delete the declaration of theview
variable.
- Where the
view
variable was, define a new variable calledbinding
that inflates theListItemSleepNightBinding
binding object, as shown below. Make the necessary import of the binding object.
- At the end of the function, instead of returning the
view
, returnbinding
.
- To get rid of the error, place your cursor on the word
binding
. PressAlt+Enter
(Option+Enter
on a Mac) to open the intention menu. Select Change parameter ‘itemView’ type of primary constructor of class ‘ViewHolder’ to ‘ListItemSleepNightBinding’. This updates the parameter type of theViewHolder
class. - Scroll up to the class definition of the
ViewHolder
to see the change in the signature. You see an error foritemView
, because you changeditemView
tobinding
in thefrom()
method. In theViewHolder
class definition, right-click on one of the occurrences ofitemView
and select Refactor > Rename. Change the name tobinding
. Prefix the constructor parameterbinding
withval
to make it a property. - In the call to the parent class,
RecyclerView.ViewHolder
, change the parameter frombinding
tobinding.root
. You need to pass aView
, andbinding.root
is the rootConstraintLayout
in your item layout.
Your finished class declaration should look like the code below.
Step 3: Replace findViewById()
You can now update the sleepLength
, quality
, and qualityImage
properties to use the binding
object instead of findViewById()
.
- Change the initializations of
sleepLength
,qualityString
, andqualityImage
to use the views of thebinding
object, as shown below. After this, your code should not show any more errors.
Once your code is compiling you can inline the new definitions. Do this by right clicking on each field, then selecting Refactor > Inline. Select Inline all references and remove the property to tell Android studio to remove the property.
Inline is a very common refactor. It’s worth taking a moment to learn the keyboard shortcut. You can find the keyboard shortcut in the context menu next to Inline.
- Right-click on the
sleepLength
,quality
, andqualityImage
property names. Select Refactor > Inline, or pressControl+Command+N
(Option+Command+N
on a Mac).
Your refactored ViewHolder
should now look like this:
Step 4: Create binding adapters
In a previous lesson, you used the Transformations
class to take LiveData
and generate formatted strings to display in text views. However, if you need to bind different types, or complex types, you can provide binding adapters to help data binding use those types. Binding adapters are adapters that take your data and adapt it into something that data binding can use to bind a view, like text or an image.
You are going to implement three binding adapters, one for the quality image, and one for each text field. In summary, to declare a binding adapter, you define a method that takes an item and a view, and annotate it with @BindingAdapter
. In the body of the method, you implement the transformation. In Kotlin, you can write a binding adapter as an extension function on the view class that receives the data.
Note that you will have to import a number of classes in the step, and it will not be called out individually.
- Open
SleepNightAdapater.kt
. Inside theViewHolder
class, find thebind()
method and remind yourself what this method does. You will take the code that calculates the values forbinding.sleepLength
,binding.quality
, andbinding.qualityImage
, and use it inside the adapter instead. (For now, leave the code as it is; you move it in a later step.) - In the
sleeptracker
package, create and open a file calledBindingUtils.kt
. Declare an extension function onTextView
, calledsetSleepDurationFormatted
, and pass in aSleepNight
. This function will be your adapter for calculating and formatting the sleep duration.
- In the body of
setSleepDurationFormatted
, bind the data to the view as you did inViewHolder.bind()
. CallconvertDurationToFormatted()
and then set thetext
of theTextView
to the formatted text. (Because this is an extension function onTextView
, you can directly access thetext
property.)
- To tell data binding about this binding adapter, annotate the function with
@BindingAdapter
. This function is the adapter for thesleepDurationFormatted
attribute, so passsleepDurationFormatted
as an argument to@BindingAdapter
.
- The second adapter sets the sleep quality based on the value in a
SleepNight
object. Create an extension function calledsetSleepQualityString()
onTextView
, and pass in aSleepNight
. In the body, bind the data to the view as you did inViewHolder.bind()
. CallconvertNumericQualityToString
and set thetext
. Annotate the function with@BindingAdapter("sleepQualityString")
.
- The third binding adapter sets the image on an image view. Create the extension function on
ImageView
, callsetSleepImage
, and use the code fromViewHolder.bind()
, as shown below.
Step 5: Update SleepNightAdapter
- Open
SleepNightAdapter.kt
. Delete everything in thebind()
method, because you can now use data binding and your new adapters to do this work for you. - Inside
bind()
, assign sleep toitem
, because you need tell the binding object about your newSleepNight
. - Below that line, add
binding.executePendingBindings()
. This call is an optimization that asks data binding to execute any pending bindings right away. It’s always a good idea to callexecutePendingBindings()
when you use binding adapters in aRecyclerView
, because it can slightly speed up sizing the views.
Step 6: Add bindings to XML layout
- Open
list_item_sleep_night.xml
. In theImageView
, add anapp
property with the same name as the binding adapter that sets the image. Pass in thesleep
variable, as shown below. This property creates the connection between the view and the binding object, via the adapter. WheneversleepImage
is referenced, the adapter will adapt the data from theSleepNight
.
- Do the same for the
sleep_length
and thequality_string
text views. WheneversleepDurationFormatted
orsleepQualityString
are referenced, the adapters will adapt the data from theSleepNight
.
- Run your app. It works exactly the same as it did before. The binding adapters take care of all the work of formatting and updating the views as the data changes, simplifying the
ViewHolder
and giving the code much better structure than it had before.
You’ve displayed the same list for the last few exercises. That’s by design, to show you that the Adapter
interface allows you to architect your code in many different ways. The more complex your code, the more important it becomes to architect it well. In production apps, these patterns and others are used with RecyclerView
. The patterns all work, and each has its benefits. Which one you choose depends on what you are building.
Using GridLayout
Layouts and LayoutManagers
In a previous task, when you added the RecyclerView
to fragment_sleep_tracker.xml
, you added a LinearLayoutManager
without any customizations. This code displays the data as a vertical list.
LinearLayoutManager
is the most common and straightforward layout manager for RecyclerView
, and it supports both horizontal and vertical placement of child views. For example, you could use LinearLayoutManager
to create a carousel of images that the user scrolls horizontally.
GridLayout
Another common use case is needing to show a lot of data to the user, which you can do using GridLayout
. The GridLayoutManager
for RecyclerView
lays out the data as a scrollable grid, as shown below.
From a design perspective, GridLayout
is best for lists that can be represented as icons or images, such as lists within a photo browsing app. In the sleep-tracker app, you could show each night of sleep as a grid of large icons. This design would give the user an overview of their sleep quality at a glance.
How GridLayout lays out items
GridLayout
arranges items in a grid of rows and columns. Assuming vertical scrolling, by default, each item in a row takes up one “span.” (In this case, one span is equivalent to the width of one column.)
In the first two examples shown below, each row is made up of three spans. By default, the GridLayoutManager
lays out each item in one span until the span count, which you specify. When it reaches the span count, it wraps to the next line.
By default, each item takes up one span, but you can make an item wider by specifying how many spans it is to occupy. For example, the top item in the rightmost screen (shown below) takes up three spans.
Tip:
Span can mean either “row” or “column.” With
GridLayoutManager
, you usespanCount
to indicate how many columns or rows a grid has, and also how much grid space an item takes up horizontally or vertically.
When you create aGridLayoutManager
, you specify the orientation separately from the number of spans, and “span” is “direction-agnostic.” In a (default) vertical configuration, “span” and “column” are equivalent.
Change LinearLayout to GridLayout
Step 1: Change the LayoutManager
- Open the
fragment_sleep_tracker.xml
layout file. Remove the layout manager from thesleep_list
RecyclerView
definition.
- Open
SleepTrackerFragment.kt
. InOnCreateView()
, just before thereturn
statement, create a new vertical, top-to-bottomGridLayoutManager
with 3 spans.
The GridLayoutManager
constructor takes up to four arguments: a context, which is the activity
, the number spans (columns, in the default vertical layout), an orientation (default is vertical), and whether it’s a reverse layout (default is false
).
- Below that line, tell the
RecyclerView
to use thisGridLayoutManager
. TheRecyclerView
is in the binding object and is calledsleepList
. (Seefragment_sleep_tracker.xml
.)
Step 2: Change the layout
The current layout in list_item_sleep_night.xml
displays the data by using a whole row per night. In this step, you define a more compact square item layout for the grid.
Tip: If you don’t want to lose your current layout, make a copy of the file first and name it list_item_sleep_night_linear.xml
, or comment out the code instead of removing it.
- Open
list_item_sleep_night.xml
. Delete thesleep_length
TextView
, because the new design doesn’t need it. - Move the
quality_string
TextView
so that it displays beneath theImageView
. To do that, you have to update quite a few things. Here is the final layout for thequality_string
TextView
andquality_image
ImageView
:
- Verify in the Design view that the
quality_string
TextView
is positioned below theImageView
.
Because you used data binding, you don’t need to change anything in the Adapter
. The code should just work, and your list should display as a grid.
- Run the app and observe how the sleep data is displayed in a grid.
Note that the ConstraintLayout
still takes the entire width. The GridLayoutManager
gives your view a fixed width, based on its span. GridLayoutManager
does its best to meet all constraints when laying out the grid, adding whitespace or clipping items.
- In
SleepTrackerFragment
, in the code that creates theGridLayoutManager
, change the number of spans forGridLayoutManger
to 1. Run the app, and you get a list.
- Change the number of spans for
GridLayoutManager
to 10 and run the app. Notice that theGridLayoutManager
will fit 10 items in a row, but the items are now clipped. - Change the span count to 5 and the direction to
GridLayoutManager.HORIZONTAL
. Run the app and notice how you can scroll horizontally. You would need a different layout to make this look good.
- Don’t forget to set the span count back to 3 and the orientation to vertical!
Interacting with List Items
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?
-
The
SleepTrackerFragment
hosts many views, and so listening to click events at the fragment level won’t tell you which item was clicked. It won’t even tell you whether it was an item that was clicked or one of the other UI elements. -
Listening at the
RecyclerView
level, it’s hard to figure out exactly what item in the list the user clicked on. -
The best pace to get information about 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, what is the best place for handling the clicks?
-
The
Adapter
displays data items in views, so you could handle clicks in the adapter. However, the adapter’s job is to adapt data for display, not deal with app logic. -
You should usually handle clicks in the
ViewModel
, because theViewModel
has access to the data and logic for determining what needs to happen in response to the click.
Tip: There are other patterns for implementing click listeners in RecyclerViews
, but the one you work with in this lesson is easier to explain and more straightforward to implement. As you work on Android apps, you’ll encounter different patterns for using click listeners in RecyclerViews
. All the patterns have their advantages.
Step 1: Create a click listener and trigger it from the item layout
- In the
sleeptracker
folder, open SleepNightAdapter.kt. At the end of the file, at the top level, create a new listener 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 will set theandroid:onClick
property of the view later to this function.) - 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. - To define what
onClick()
does, provide aclickListener
callback in the constructor ofSleepNightListener
and assign it toonClick()
. Giving the lambda that handles the click a name,clickListener
, helps keep track of it as it is passed between classes. TheclickListener
callback only needs thenight.nightId
to access data from the database. Your finishedSleepNightListener
class should look like the code below.
- Open 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.
- To listen for clicks on any part of this list item, add the
android:onClick
attribute to theConstraintLayout
. Set the attribute toclickListener.onClick(sleep)
using a data binding lambda, as shown below:
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.
- 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.
- Add the
clickListener
parameter tobind()
. To do this, put the cursor on the error, and pressAlt+Enter
(Windows) orOption+Enter
(Mac) on the error. - Inside the
ViewHolder
class, inside thebind()
function, assign the click listener to thebinding
object. You see an error because you need to update the binding object.
- 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.
- 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.
- 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.
Navigate on Click
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.
Before starting, open fragment_sleep_detail.xml
and uncomment the code inside the ConstraintLayout
.
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
SleepTrackerViewModel
, towards the end, define theonSleepNightClicked()
click handler function. - Inside the
onSleepNightClicked()
, trigger navigation by setting_navigateToSleepDetail
to the passed inid
of the clicked sleep night.
- Implement
_navigateToSleepDetail
. As you’ve done before, define aprivate MutableLiveData
for the navigation state. And a public gettableval
to go with it.
- Define the method to call after the app has finished navigating. Call it
onSleepDetailNavigated()
and set its value tonull
.
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.
- 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. This leaves you with an error, because you haven’t definedonSleepNightClicked()
yet. You can keep, comment out, or delete the toast, as you wish.
Add the code to observe clicks
Open SleepTrackerFragment.kt. 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 lesson, here is the code:
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 foritem
. In particular, when the app starts, theLiveData
starts asnull
, 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.
Adding Headers to the RecyclerView
One common example is having headers in your list or grid. A list can have a single header to describe the item content. A list can also have multiple headers to group and separate items in a single list.
RecyclerView
doesn’t know anything about your data or what type of layout each item has. The LayoutManager
arranges the items on the screen, but the adapter adapts the data to be displayed and passes view holders to the RecyclerView
. So you will add the code to create headers in the adapter.
Two ways of adding headers
In RecyclerView
, every item in the list corresponds to an index number starting from 0. For example:
[Actual Data] → [Adapter Views]
[0: SleepNight] → [0: SleepNight]
[1: SleepNight] → [1: SleepNight]
[2: SleepNight] → [2: SleepNight]
One way to add headers to a list is to modify your adapter to use a different ViewHolder
by checking indexes where your header needs to be shown. The Adapter
will be responsible for keeping track of the header. For example, to show a header at the top of the table, you need to return a different ViewHolder
for the header while laying out the zero-indexed item. Then all the other items would be mapped with the header offset, as shown below.
[Actual Data] → [Adapter Views]
[0: Header]
[0: SleepNight] → [1: SleepNight]
[1: SleepNight] → [2: SleepNight]
[2: SleepNight] → [3: SleepNight.
Another way to add headers is to modify the backing dataset for your data grid. Since all the data that needs to be displayed is stored in a list, you can modify the list to include items to represent a header. This is a bit simpler to understand, but it requires you to think about how you design your objects, so you can combine the different item types into a single list. Implemented this way, the adapter will display the items passed to it. So the item at position 0 is a header, and the item at position 1 is a SleepNight
, which maps directly to what’s on the screen.
[Actual Data] → [Adapter Views]
[0: Header] → [0: Header]
[1: SleepNight] → [1: SleepNight]
[2: SleepNight] → [2: SleepNight]
[3: SleepNight] → [3: SleepNight]
Each methodology has benefits and drawbacks. Changing the dataset doesn’t introduce much change to the rest of the adapter code, and you can add header logic by manipulating the list of data. On the other hand, using a different ViewHolder
by checking indexes for headers gives more freedom on the layout of the header. It also lets the adapter handle how data is adapted to the view without modifying the backing data.
In this lesson, you update your RecyclerView
to display a header at the start of the list. In this case, your app will use a different ViewHolder
for the header than for data items. The app will check the index of the list to determine which ViewHolder
to use.
Add a List Header
Step 1: Create a DataItem
class
To abstract the type of item and let the adapter just deal with “items”, you can create a data holder class that represents either a SleepNight
or a Header
. Your dataset will then be a list of data holder items.
- Open SleepNightAdapter.kt. Below the
SleepNightListener
class, at the top level, define asealed
class calledDataItem
that represents an item of data.
Note:
A sealed
class defines a closed type, which means that all subclasses of DataItem
must be defined in this file. As a result, the number of subclasses is known to the compiler. It’s not possible for another part of your code to define a new type of DataItem
that could break your adapter.
- Inside the body of the
DataItem
class, define two classes that represent the different types of data items. The first is aSleepNightItem
, which is a wrapper around aSleepNight
, so it takes a single value calledsleepNight
. To make it part of the sealed class, have it extendDataItem
.
- The second class is
Header
, to represent a header. Since a header has no actual data, you can declare it as anobject
. That means there will only ever be one instance ofHeader
. Again, have it extendDataItem
.
- Inside
DataItem
, at the class level, define anabstract
Long
property namedid
. When the adapter usesDiffUtil
to determine whether and how an item has changed, theDiffItemCallback
needs to know the id of each item. You will see an error, becauseSleepNightItem
andHeader
need to override the abstract propertyid
.
- In
SleepNightItem
, overrideid
to return thenightId
.
- In
Header
, overrideid
to returnLong.MIN_VALUE
, which is a very, very small number (literally, -2 to the power of 63). So, this will never conflict with anynightId
in existence.
Your finished code should look like this, and your app should build without errors.
Step 2: Create a ViewHolder
for the Header
- Create the layout for the header in a new layout resource file called header.xml that displays a
TextView
. There is nothing exciting about this, so here is the code.
- Extract
"Sleep Results"
into a string resource and call itheader_text
.
- In SleepNightAdapter.kt, inside
SleepNightAdapter
, above theViewHolder
class, create a newTextViewHolder
class. This class inflates the textview.xml layout, and returns aTextViewHolder
instance. Since you’ve done this before, here is the code, and you’ll have to importView
andR
:
Step 3: Update SleepNightAdapter
Next you need to update the declaration of SleepNightAdapter
. Instead of only supporting one type of ViewHolder
, it needs to be able to use any type of view holder.
Define the types of items
- In
SleepNightAdapter.kt
, at the top level, below theimport
statements and aboveSleepNightAdapter
, define two constants for the view types.
The RecyclerView
will need to distinguish each item’s view type, so that it can correctly assign a view holder to it.
- Inside the
SleepNightAdapter
, create a function to overridegetItemViewType()
to return the right header or item constant depending on the type of the current item.
Update the SleepNightAdapter
definition
- In the definition of
SleepNightAdapter
, update the first argument for theListAdapter
fromSleepNight
toDataItem
. - In the definition of
SleepNightAdapter
, change the second generic argument for theListAdapter
fromSleepNightAdapter.ViewHolder
toRecyclerView.ViewHolder
. You will see some errors for necessary updates, and your class header should look like shown below.
Update onCreateViewHolder()
- Change the signature of
onCreateViewHolder()
to return aRecyclerView.ViewHolder
.
- Expand the implementation of the
onCreateViewHolder()
method to test for and return the appropriate view holder for each item type. Your updated method should look like the code below.
Update onBindViewHolder()
- Change the parameter type of
onBindViewHolder()
fromViewHolder
toRecyclerView.ViewHolder
.
- Add a condition to only assign data to the view holder if the holder is a
ViewHolder
. - Cast the object type returned by
getItem()
toDataItem.SleepNightItem
. Your finishedonBindViewHolder()
function should look like this.
Update the diffUtil
callbacks
Change the methods in SleepNightDiffCallback
to use your new DataItem
class instead of the SleepNight
. Suppress the lint warning as shown in the code below.
Add and submit the header
- Inside the
SleepNightAdapter
, belowonCreateViewHolder()
, define a functionaddHeaderAndSubmitList()
as shown below. This function takes a list ofSleepNight
. Instead of usingsubmitList()
, provided by theListAdapter
, to submit your list, you will use this function to add a header and then submit the list.
- Inside
addHeaderAndSubmitList()
, if the passed in list isnull
, return just a header, otherwise, attach the header to the head of the list, and then submit the list.
- Open SleepTrackerFragment.kt and change the call to
submitList()
toaddHeaderAndSubmitList()
.
There are two things that need to be fixed for this app. One is visible, and one is not.
-
The header shows up in the top-left corner, and is not easily distinguishable.
-
It doesn’t matter much for a short list with one header, but you should not do list manipulation in
addHeaderAndSubmitList()
on the UI thread. Imagine a list with hundreds of items, multiple headers, and logic to decide where items need to be inserted. This work belongs in a coroutine.
Change addHeaderAndSubmitList()
to use coroutines:
- At the top level inside the
SleepNightAdapter
class, define aCoroutineScope
withDispatchers.Default
.
- In
addHeaderAndSubmitList()
, launch a coroutine in theadapterScope
to manipulate the list. Then switch to theDispatchers.Main
context to submit the list, as shown in the code below.
Extend the header to span across the screen
Currently, the header is the same width as the other items on the grid, taking up one span horizontally and vertically. The whole grid fits three items of one span width horizontally, so the header should use three spans horizontally.
To fix the header width, you need to tell the GridLayoutManager
when to span the data across all the columns. You can do this by configuring the SpanSizeLookup
on a GridLayoutManager
. This is a configuration object that the GridLayoutManager
uses to determine how many spans to use for each item in the list.
- Open SleepTrackerFragment.kt. Find the code where you define
manager
, towards the end ofonCreateView()
.
- Below
manager
, definemanager.spanSizeLookup
, as shown. You need to make anobject
becausesetSpanSizeLookup
doesn’t take a lambda. To make anobject
in Kotlin, typeobject : classname
, in this caseGridLayoutManager.SpanSizeLookup
.
-
You might get a compiler error to call the constructor. If you do, open the intention menu with
Option+Enter
(Mac) orAlt+Enter
(Windows) to apply the constructor call. -
Then you’ll get an error on
object
saying you need to override methods. Put the cursor onobject
, pressOption+Enter
(Mac) orAlt+Enter
(Windows) to open the intentions menu, then override the methodgetSpanSize()
. -
In the body of
getSpanSize()
, return the right span size for each position. Position 0 has a span size of 3, and the other positions have a span size of 1. Your completed code should look like the code below:
- To improve how your header looks, open header.xml and add this code to the layout file header.xml.
Summary of Adding Headers
-
A header is generally an item that spans the width of a list and acts as a title or separator. A list can have a single header to describe the item content, or multiple headers to group items and separate items from each other.
-
A
RecyclerView
can use multiple view holders to accommodate a heterogeneous set of items; for example, headers and list items. -
One way to add headers is to modify your adapter to use a different
ViewHolder
by checking indexes where your header needs to be shown. TheAdapter
is responsible for keeping track of the header. -
Another way to add headers is to modify the backing dataset (the list) for your data grid, which is what you did in this lesson.
These are the major steps for adding a header:
-
Abstract the data in your list by creating a
DataItem
that can hold a header or data. -
Create a view holder with a layout for the header in the adapter.
-
Update the adapter and its methods to use any kind of
RecyclerView.ViewHolder
. -
In
onCreateViewHolder()
, return the correct type of view holder for the data item. -
Update
SleepNightDiffCallback
to work with theDataItem
class. -
Create a
addHeaderAndSubmitList()
function that uses coroutines to add the header to the dataset and then callssubmitList()
. -
Implement
GridLayoutManager.SpanSizeLookup()
to make only the header three spans wide.
Summary
RecyclerView fundamentals
-
Displaying a list or grid of data is one of the most common UI tasks in Android.
RecyclerView
is designed to be efficient even when displaying extremely large lists. -
RecyclerView
does only the work necessary to process or draw items that are currently visible on the screen. -
When an item scrolls off the screen, its views are recycled. That means the item is filled with new content that scrolls onto the screen.
-
The adapter pattern in software engineering helps an object work together with another API.
RecyclerView
uses an adapter to transform app data into something it can display, without the need for changing how the app stores and processes data.
To display your data in a RecyclerView
, you need the following parts:
RecyclerView
- To create an instance of
RecyclerView
, define a<RecyclerView>
element in the layout file.
LayoutManager
A RecyclerView
uses a LayoutManager
to organize the layout of the items in the RecyclerView
, such as laying them out in a grid or in a linear list.
In the <RecyclerView>
in the layout file, set the app:layoutManager
attribute to the layout manager (such as LinearLayoutManager
or GridLayoutManager
).
You can also set the LayoutManager
for a RecyclerView
programmatically. (This technique is covered in a later codelab.)
Layout for each item
Create a layout for one item of data in an XML layout file.
Adapter
Create an adapter that prepares the data and how it will be displayed in a ViewHolder
. Associate the adapter with the RecyclerView
.
When RecyclerView
runs, it will use the adapter to figure out how to display the data on the screen.
The adapter requires you to implement the following methods:
– getItemCount()
to return the number of items.
– onCreateViewHolder()
to return the ViewHolder
for an item in the list.
– onBindViewHolder()
to adapt the data to the views for an item in the list.
ViewHolder
A ViewHolder
contains the view information for displaying one item from the item’s layout.
-
The
onBindViewHolder()
method in the adapter adapts the data to the views. You always override this method. Typically,onBindViewHolder()
inflates the layout for an item, and puts the data in the views in the layout. -
Because the
RecyclerView
knows nothing about the data, theAdapter
needs to inform theRecyclerView
when that data changes. UsenotifyDataSetChanged()
to notify theAdapter
that the data has changed.
DiffUtil and data binding with RecyclerView
DiffUtil:
-
RecyclerView
has a class calledDiffUtil
which is for calculating the differences between two lists. -
DiffUtil
has a class calledItemCallBack
that you extend in order to figure out the difference between two lists. -
In the
ItemCallback
class, you must override theareItemsTheSame()
andareContentsTheSame()
methods.
ListAdapter:
-
To get some list management for free, you can use the
ListAdapter
class instead ofRecyclerView.Adapter
. However, if you useListAdapter
you have to write your own adapter for other layouts, which is why this codelab shows you how to do it. -
To open the intention menu in Android Studio, place the cursor on any item of code and press
Alt+Enter
(Option+Enter
on a Mac). This menu is particularly helpful for refactoring code and creating stubs for implementing methods. The menu is context-sensitive, so you need to place cursor exactly to get the correct menu.
Data binding:
- Use data binding in the item layout to bind data to the views.
Binding adapters:
-
You previously used
Transformations
to create strings from data. If you need to bind data of different or complex types, provide binding adapters to help data binding use them. -
To declare a binding adapter, define a method that takes an item and a view, and annotate the method with
@BindingAdapter
. In Kotlin, you can write the binding adapter as an extension function on theView
. Pass in the name of the property that the adapter adapts. For example:
- In the XML layout, set an
app
property with the same name as the binding adapter. Pass in a variable with the data. For example:
GridLayout with RecyclerView
-
Layout managers manage how the items in the
RecyclerView
are arranged. -
RecyclerView
comes with out-of-the-box layout managers for common use cases such asLinearLayout
for horizontal and vertical lists, andGridLayout
for grids. -
For more complicated use cases, implement a custom
LayoutManager
. -
From a design perspective,
GridLayout
is best used for lists of items that can be represented as icons or images. -
GridLayout
arranges items in a grid of rows and columns. Assuming vertical scrolling, each item in a row takes up what’s called a “span.” -
You can customize how many spans an item takes up, creating more interesting grids without the need for a custom layout manager.
-
Create an item layout for one item in the grid, and the layout manager takes care of arranging the items.
-
You can set the
LayoutManager
for theRecyclerView
either in the XML layout file that contains the<RecyclerView>
element, or programmatically.
Interacting with RecyclerView items
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.
- Set the click listener on the view.
- Pass the click listener to the adapter constructor, into the view holder, and add it to the binding object.
- In the fragment that shows the recycler view, where you create the adapter, define a click listener by passing a lambda to the adapter.
- Implement the click handler in the view model. For clicks on list items, this commonly triggers navigation to a detail fragment.
Headers in RecyclerView
-
A header is generally an item that spans the width of a list and acts as a title or separator. A list can have a single header to describe the item content, or multiple headers to group items and separate items from each other.
-
A
RecyclerView
can use multiple view holders to accommodate a heterogeneous set of items; for example, headers and list items. -
One way to add headers is to modify your adapter to use a different
ViewHolder
by checking indexes where your header needs to be shown. TheAdapter
is responsible for keeping track of the header. -
Another way to add headers is to modify the backing dataset (the list) for your data grid, which is what you did in this codelab.
These are the major steps for adding a header:
-
Abstract the data in your list by creating a
DataItem
that can hold a header or data. -
Create a view holder with a layout for the header in the adapter.
-
Update the adapter and its methods to use any kind of
RecyclerView.ViewHolder
. -
In
onCreateViewHolder()
, return the correct type of view holder for the data item. -
Update
SleepNightDiffCallback
to work with theDataItem
class. -
Create a
addHeaderAndSubmitList()
function that uses coroutines to add the header to the dataset and then callssubmitList()
. -
Implement
GridLayoutManager.SpanSizeLookup()
to make only the header three spans wide.