This codelab is part of the Android Kotlin Fundamentals course. You'll get the most value out of this course if you work through the codelabs in sequence. All the course codelabs are listed on the Android Kotlin Fundamentals codelabs landing page.
Introduction
This codelab teaches you how to use a RecyclerView
to display lists of items. Building on the sleep-tracker app from the previous series of codelabs, you learn a better and more versatile way to display data, using 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, view model factories, 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.
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 does not scale. After you fix all these problems in this codelab, the final app has the same functionality, and the main screen looks like this:
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 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
.
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
.
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 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. - 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.
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 Text 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 that 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. 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.
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.
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 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 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.
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. 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
.
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 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.
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 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.
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()
- Associate the
adapter
with theRecyclerView
.
binding.sleepList.adapter = adapter
- 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.)
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 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
. - In
onCreateView()
, 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.
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 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()
.
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.
AsRecyclerView
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
.
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
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 updatedonCreateViewHolder()
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()
, 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
- 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.
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()
.
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
})
}
- 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:
private fun ViewHolder.bind(item: SleepNight) {...}
- 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.
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
})
}
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 completedonCreateViewHolder()
andfrom()
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.
Android Studio project: RecyclerViewFundamentals
- 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 ofRecyclerView
, define a<RecyclerView>
element in the layout file. - LayoutManager
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. (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 aViewHolder
. Associate the adapter with theRecyclerView
.
WhenRecyclerView
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 theViewHolder
for an item in the list.
–onBindViewHolder()
to adapt the data to the views for an item in the list. - ViewHolder
AViewHolder
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.
Udacity course:
Android developer documentation:
This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:
- 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
.
Start the next lesson: