1. Welcome
Introduction
This codelab teaches you how to use a RecyclerView
to display lists of items. Building on the concepts learned in the sleep-tracker app from the previous series of codelabs, you learn a better and more versatile way to display data. Your new app will update the sleep-tracker to use a RecyclerView
with a recommended architecture.
What you should already know
You should be familiar with:
- Building a basic user interface (UI) using an activity, fragments, and views.
- Navigating between fragments, and using
safeArgs
to pass data between fragments. - Using view models, transformations, and
LiveData
and their observers. - Creating a
Room
database, creating a DAO, and defining entities. - Using coroutines for database tasks and other long-running tasks.
What you'll learn
- How to use a
RecyclerView
with anAdapter
and aViewHolder
to display a list of items.
What you'll do
- Change the TrackMySleepQuality app from the previous lesson to use a
RecyclerView
to display sleep-quality data.
2. App overview
In this codelab, you build the RecyclerView
portion of an app that tracks sleep quality. The app uses a Room
database to store sleep data over time.
The starter sleep-tracker app has two screens, represented by fragments, as shown in the figure below.
The first screen, shown on the left, has buttons to start and stop tracking. This screen also shows all 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, ViewModel
, and LiveData
. The app also uses a Room
database to make sleep data persistent.
The list of sleep nights displayed in the first screen is functional, but not pretty. The app uses a complex formatter to create text strings for the text view and numbers for the quality. Also, this design is a bit complex which decreases our scalability. After you fix all these problems in this codelab, the final app has the same functionality as the original app but the improved main screen is easier to read:
3. Concept: 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.
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 as it scrolls onto the screen. This
RecyclerView
behavior saves a lot of processing time and helps lists scroll smoothly. - When an item changes, instead of redrawing the entire list,
RecyclerView
can update that one item. This is a huge efficiency gain when displaying long 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
.
The adapter pattern
If you ever travel between countries that use different electric sockets, you probably know how you can plug your devices into foreign 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 uses a similar concept. This pattern allows the API of one class to be used as 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
.
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. The layout manager handles the organization (the layout) of UI components in a view.
- A view holder. The view holder extends the
ViewHolder
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. - An adapter. The adapter connects your data to the
RecyclerView
. 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.
4. Task: Implement RecyclerView and an Adapter
In this task, you add a RecyclerView
to your layout file and set up an Adapter
to expose sleep data to the RecyclerView
.
Step 1: Add RecyclerView with LayoutManager
In this step, you replace the ScrollView
with a RecyclerView
in the fragment_sleep_tracker.xml
file.
- Download the RecyclerViewFundamentals-Starter app from GitHub.
- Build and run the app. Notice how the data is displayed as simple text.
- Open the
fragment_sleep_tracker.xml
layout file in the Design tab in Android Studio. - In the Component Tree pane, delete the
ScrollView
. This action also deletes theTextView
that's inside theScrollView
. - In the Palette pane, scroll through the list of component types on the left to find Containers, then select it.
- Drag a
RecyclerView
from the Palette pane to the Component Tree pane. Place theRecyclerView
inside theConstraintLayout
.
- If a dialog opens asking whether you want to add a dependency, click OK to let Android Studio add the
recyclerview
dependency to your Gradle file. It may take a few seconds, and then your app syncs.
- Open the module
build.gradle
file, scroll to the end, and take note of the new dependency, which looks similar to the code below:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
- Switch back to
fragment_sleep_tracker.xml
. - In the Code tab, look for the
RecyclerView
code shown below:
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
- Give the
RecyclerView
anid
ofsleep_list
.
android:id="@+id/sleep_list"
- Position the
RecyclerView
to take up the remaining portion of the screen inside theConstraintLayout
. To do this, constrain the top of theRecyclerView
to the Start button, the bottom to the Clear button, and each side to the parent. Set the layout width and height to 0 dp in the Layout Editor or in XML, using the following code:
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/clear_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stop_button"
- 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.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
- Switch to the Design tab and notice the added constraints have caused the
RecyclerView
to expand to fill the available space.
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
.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:textSize="24sp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
- 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.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
- 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. With both of these pieces, 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.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
- At the top level of
SleepNightAdapter
, create alistOf
SleepNight
variable to hold the data.
var data = listOf<SleepNight>()
- 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()
.
override fun getItemCount() = data.size
- In
SleepNightAdapter
, override theonBindViewHolder()
function, as shown below.
The onBindViewHolder()
function is called by RecyclerView
to display the data for one list item at the specified position. So the onBindViewHolder()
method takes two arguments: a view holder, and a position of the data to bind. For this app, the holder is the TextItemViewHolder
, and the position is the position in the list.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
- Inside
onBindViewHolder()
, create a variable for one item at a given position in the data.
val item = data[position]
- The
ViewHolder
you just 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.
holder.textView.text = item.sleepQuality.toString()
- In
SleepNightAdapter
, override and implementonCreateViewHolder()
, which is called when theRecyclerView
needs a view holder.
This function takes two parameters and returns a ViewHolder
. The parent
parameter, which is the view group that holds the view holder, is always the RecyclerView
. The viewType
parameter is used when there are multiple views in the same RecyclerView
. For example, if you put a list of text views, an image, and a video all in the same RecyclerView
, the onCreateViewHolder()
function would need to know what type of view to use.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
- In
onCreateViewHolder()
, create an instance ofLayoutInflater
.
The layout inflater knows how to create views from XML layouts. The context
contains information on how to correctly inflate the view. In an adapter for a recycler view, you always pass in the context of the parent
view group, which is the RecyclerView
.
val layoutInflater = LayoutInflater.from(parent.context)
- In
onCreateViewHolder()
, create theview
by asking thelayoutinflater
to inflate it.
Pass in the XML layout for the view, and the parent
view group for the view. The third, boolean, argument is attachToRoot
. This argument needs to be false
, because RecyclerView
adds this item to the view hierarchy for you when it's time.
val view = layoutInflater
.inflate(R.layout.text_item_view, parent, false) as TextView
- In
onCreateViewHolder()
, return aTextItemViewHolder
made withview
.
return TextItemViewHolder(view)
- 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 the RecyclerView
when the data that it's displaying has changed, add a custom setter to the data
variable that's at the top of the SleepNightAdapter
class. In the setter, give data
a new value, then call notifyDataSetChanged()
to trigger redrawing the list with the new data.
var data = listOf<SleepNight>()
set(value) {
field = value
notifyDataSetChanged()
}
Step 4: Tell RecyclerView about the Adapter
The RecyclerView
needs to know about the adapter to use to get view holders.
- Open
SleepTrackerFragment.kt
. - In
onCreateview()
, create an adapter. Put this code after the creation of theViewModel
model, and before thereturn
statement.
val adapter = SleepNightAdapter()
- After you get a reference to the binding object, associate the
adapter
with theRecyclerView
.
binding.sleepList.adapter = adapter
- Clean and rebuild your project to update the
binding
object.
If you still see errors around binding.sleepList
or binding.FragmentSleepTrackerBinding
, invalidate caches and restart. (Select File > Invalidate Caches / Restart.)
If you run the app now, there are no errors, but you won't see any data displayed when you tap Start, then Stop.
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 the
nights
variable, which stores all the sleep nights, which is the data to display. Thenights
variable is set by callinggetAllNights()
on the database. - Remove
private
fromnights
, because you will create an observer that needs to access this variable. Your declaration should look like this:
val nights = database.getAllNights()
- In the
database
package, open theSleepDatabaseDao
. - Find the
getAllNights()
function. Notice 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
. - In
onCreateView()
, after the ViewModel is instantiated and you have its reference,, create an observer on thenights
variable.
By supplying the fragment's viewLifecycleOwner
as the lifecycle owner, you can make sure this observer is only active when the RecyclerView
is on the screen.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
})
- 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:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.data = it
}
})
- Build and run your code.
You'll see the sleep-quality numbers as a list, if your adapter is working. The screenshot on the left shows -1 after you tap Start. The screenshot on the right shows the updated sleep-quality number after you tap Stop and select a quality rating.
Step 6: Explore how view holders are recycled
RecyclerView
recycles view holders, which means that it reuses them. As a view scrolls off the screen, RecyclerView
reuses the view for the item 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 at the end ofonBindViewHolder()
.
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
}
- 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.
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
} else {
// reset
holder.textView.setTextColor(Color.BLACK) // black
}
- Run the app, and the numbers should always have the correct color.
Congratulations! You now have a fully functional basic RecyclerView
.
5. Task: Create a ViewHolder for all the sleep data
In this task, you replace the simple view holder with one that can display more data for a sleep night.
The simple ViewHolder
that you added to Util.kt
just wraps a TextView
in a TextItemViewHolder
.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
So why does RecyclerView
not just use a TextView
directly? This one line of code provides a lot of functionality. A ViewHolder
describes an item view and metadata about its place within the RecyclerView
. 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 the Adapter
.
If RecyclerView
does need to access the views stored in the ViewHolder
, it can do so using the view holder's itemView
property. RecyclerView
uses itemView
when it's binding an item to display on the screen, when drawing decorations around a view like a border, and for implementing accessibility.
Step 1: Create the item layout
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.
- 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.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/quality_image"
android:layout_width="@dimen/icon_size"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_sleep_5" />
<TextView
android:id="@+id/sleep_length"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/quality_image"
app:layout_constraintTop_toTopOf="@+id/quality_image"
tools:text="Wednesday" />
<TextView
android:id="@+id/quality_string"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="@+id/sleep_length"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/sleep_length"
app:layout_constraintTop_toBottomOf="@+id/sleep_length"
tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
- Switch to the Design tab in Android Studio. In Design view, your layout looks like the screenshot on the left below. In Blueprint view, it looks like the screenshot on the right.
Step 2: Create ViewHolder
- Open
SleepNightAdapter.kt
. - Make a class inside the
SleepNightAdapter
calledViewHolder
and make it extendRecyclerView.ViewHolder
.
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
- 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.)
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)
Step 3: Use the ViewHolder in SleepNightAdapter
- In the
SleepNightAdapter
class signature definition, instead ofTextItemViewHolder
, use theSleepNightAdapter.ViewHolder
that you just created.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {
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 updated onCreateViewHolder()
function:
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater =
LayoutInflater.from(parent.context)
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night,
parent, false)
return ViewHolder(view)
}
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.
val res = holder.itemView.context.resources
- 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.
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
- 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()
inSleepNightAdapter.kt
, useconvertNumericQualityToString()
to set the quality.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
- You may need to manually import these functions.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
- Immediately after you set the quality, set the correct icon for the quality. The new
ic_sleep_active
icon is provided for you in the starter code.
holder.qualityImage.setImageResource(when (item.sleepQuality) {
0 -> R.drawable.ic_sleep_0
1 -> R.drawable.ic_sleep_1
2 -> R.drawable.ic_sleep_2
3 -> R.drawable.ic_sleep_3
4 -> R.drawable.ic_sleep_4
5 -> R.drawable.ic_sleep_5
else -> R.drawable.ic_sleep_active
})
- Here is the finished updated
onBindViewHolder()
function, setting all the data for theViewHolder
:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
val res = holder.itemView.context.resources
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
holder.qualityImage.setImageResource(when (item.sleepQuality) {
0 -> R.drawable.ic_sleep_0
1 -> R.drawable.ic_sleep_1
2 -> R.drawable.ic_sleep_2
3 -> R.drawable.ic_sleep_3
4 -> R.drawable.ic_sleep_4
5 -> R.drawable.ic_sleep_5
else -> R.drawable.ic_sleep_active
})
}
- Run your app. Your display should look like the screenshot below, showing the sleep-quality icon, along with text for the sleep duration and the sleep quality.
6. Task: Improve your code
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.kt
, in the functiononBindViewHolder()
, 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.
The bind()
function is placed below onBindViewHolder()
.
private fun bind(holder: ViewHolder, item: SleepNight) {
val res = holder.itemView.context.resources
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
holder.qualityImage.setImageResource(when (item.sleepQuality) {
0 -> R.drawable.ic_sleep_0
1 -> R.drawable.ic_sleep_1
2 -> R.drawable.ic_sleep_2
3 -> R.drawable.ic_sleep_3
4 -> R.drawable.ic_sleep_4
5 -> R.drawable.ic_sleep_5
else -> R.drawable.ic_sleep_active
})
}
- Create an extension function with the following signature:
private fun ViewHolder.bind(item: SleepNight) {...}
- Cut and paste this
ViewHolder.bind()
function into theViewHolder
inner class at the bottom ofSleepNightAdapter.kt
. - 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.
fun bind(item: SleepNight) {
val res = itemView.context.resources
sleepLength.text = convertDurationToFormatted(
item.startTimeMilli, item.endTimeMilli, res)
quality.text = convertNumericQualityToString(
item.sleepQuality, res)
qualityImage.setImageResource(when (item.sleepQuality) {
0 -> R.drawable.ic_sleep_0
1 -> R.drawable.ic_sleep_1
2 -> R.drawable.ic_sleep_2
3 -> R.drawable.ic_sleep_3
4 -> R.drawable.ic_sleep_4
5 -> R.drawable.ic_sleep_5
else -> R.drawable.ic_sleep_active
})
}
Now in SleepNightAdapter.kt
, the onBindViewHolder()
function shows an unresolved reference error on bind(holder, item)
. This function is now part of the ViewHolder inner class so we need to specify the specific ViewHolder. This also means we can remove the first argument.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.bind(item)
}
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 function
from
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. The
from()
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.
override fun onCreateViewHolder(parent: ViewGroup, viewType:
Int): ViewHolder {
return ViewHolder.from(parent)
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)
return ViewHolder(view)
}
}
- 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.
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
- Run the app. Your app should build and run the same as before, which is the desired result after refactoring.
7. Solution code
Android Studio project: RecyclerViewFundamentals
8. Summary
- 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 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 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 theRecyclerView
.
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.
9. Learn more
Udacity course:
Android developer documentation:
10. 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
How does RecyclerView
display items? Select all that apply.
▢ Displays items in a list or a grid.
▢ Scrolls vertically or horizontally.
▢ Scrolls diagonally on larger devices such as tablets.
▢ Allows custom layouts when a list or a grid is not enough for the use case.
Question 2
What are the benefits of using RecyclerView
? Select all that apply.
▢ Efficiently displays large lists.
▢ Automatically updates the data.
▢ Minimizes the need for refreshes when an item is updated, deleted, or added to the list.
▢ Reuses view that scrolls off screen to display the next item that scrolls on screen.
Question 3
What are some of the reasons for using adapters? Select all that apply.
▢ Separation of concerns makes it easier to change and test code.
▢ RecyclerView
is agnostic to the data that is being displayed.
▢ Data processing layers do not have to concern themselves with how data will be displayed.
▢ The app will run faster.
Question 4
Which of the following are true of ViewHolder
? Select all that apply.
▢ The ViewHolder
layout is defined in XML layout files.
▢ There is one ViewHolder
for each unit of data in the dataset.
▢ You can have more than one ViewHolder
in a RecyclerView
.
▢ The Adapter
binds data to the ViewHolder
.