Android Kotlin Fundamentals 07.1: RecyclerView fundamentals

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 an Adapter and a ViewHolder 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 the ViewHolder class. It contains the view information for displaying one item from the item's layout. View holders also add information that RecyclerView 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 a ViewHolder. A RecyclerView 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.

  1. Download the RecyclerViewFundamentals-Starter app from GitHub.
  2. Build and run the app. Notice how the data is displayed as simple text.
  3. Open the fragment_sleep_tracker.xml layout file in the Design tab in Android Studio.
  4. In the Component Tree pane, delete the ScrollView. This action also deletes the TextView that's inside the ScrollView.
  5. In the Palette pane, scroll through the list of component types on the left to find Containers, then select it.
  6. Drag a RecyclerView from the Palette pane to the Component Tree pane. Place the RecyclerView inside the ConstraintLayout.

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

  1. 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'
  1. Switch back to fragment_sleep_tracker.xml.
  2. 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" />
  1. Give the RecyclerView an id of sleep_list.
android:id="@+id/sleep_list"
  1. Position the RecyclerView to take up the remaining portion of the screen inside the ConstraintLayout. To do this, constrain the top of the RecyclerView 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"
  1. Add a layout manager to the RecyclerView XML. Every RecyclerView needs a layout manager that tells it how to position items in the list. Android provides a LinearLayoutManager, which by default lays out the items in a vertical list of full width rows.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. 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.)

  1. 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.
  2. In text_item_view.xml, delete all the given code.
  3. Add a TextView with 16dp padding at the start and end, and a text size of 24sp. Let the width match the parent, and the height wrap the content. Because this view is displayed inside the RecyclerView, you don't have to place the view inside a ViewGroup.
<?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" />
  1. Open Util.kt. Scroll to the end and add the definition that's shown below, which creates the TextItemViewHolder class. Put the code at the bottom of the file, after the last closing brace. The code goes in Util.kt because this view holder is temporary, and you replace it later.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. If you are prompted, import android.widget.TextView and androidx.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.

  1. In the sleeptracker package, create a new Kotlin class called SleepNightAdapter.
  2. Make the SleepNightAdapter class extend RecyclerView.Adapter. The class is called SleepNightAdapter because it adapts a SleepNight object into something that RecyclerView can use. The adapter needs to know what view holder to use, so pass in TextItemViewHolder. Import necessary components when prompted, and then you'll see an error, because there are mandatory methods to implement.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. At the top level of SleepNightAdapter, create a listOf SleepNight variable to hold the data.
var data =  listOf<SleepNight>()
  1. In SleepNightAdapter, override getItemCount() to return the size of the list of sleep nights in data. The RecyclerView needs to know how many items the adapter has for it to display, and it does that by calling getItemCount().
override fun getItemCount() = data.size
  1. In SleepNightAdapter, override the onBindViewHolder() 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) {
}
  1. Inside onBindViewHolder(), create a variable for one item at a given position in the data.
 val item = data[position]
  1. The ViewHolder you created has a property called textView. Inside onBindViewHolder(), set the text of the textView 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()
  1. In SleepNightAdapter, override and implement onCreateViewHolder(), which is called when the RecyclerView needs a view holder to represent an item.

    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 {
}
  1. In onCreateViewHolder(), create an instance of LayoutInflater.

    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)
  1. In onCreateViewHolder(), create the view by asking the layoutinflater 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
  1. In onCreateViewHolder(), return a TextItemViewHolder made with view.
return TextItemViewHolder(view)
  1. The adapter needs to let the RecyclerView know when the data has changed, because the RecyclerView 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.

  1. Open SleepTrackerFragment.kt.
  2. In onCreateview(), create an adapter. Put this code after the creation of the ViewModel model, and before the return statement.
val adapter = SleepNightAdapter()
  1. Associate the adapter with the RecyclerView.
binding.sleepList.adapter = adapter
  1. 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.

  1. Open SleepTrackerViewModel.
  2. Find the nights variable, which stores all the sleep nights, which is the data to display. The nights variable is set by calling getAllNights() on the database.
  3. Remove private from nights, because you will create an observer that needs to access this variable. Your declaration should look like this:
val nights = database.getAllNights()
  1. In the database package, open the SleepDatabaseDao.
  2. Find the getAllNights() function. Notice that this function returns a list of SleepNight values as LiveData. This means that the nights variable contains LiveData that is kept updated by Room, and you can observe nights to know when it changes.
  3. Open SleepTrackerFragment.
  4. In onCreateView(), below the creation of the adapter, create an observer on the nights 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 {
   })
  1. Inside the observer, whenever you get a non-null value (for nights), assign the value to the adapter's data. This is the completed code for the observer and setting the data:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. 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.

  1. In the SleepNightAdapter class, add the following code to at the end of onBindViewHolder().
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. Run the app.
  2. Add some low sleep-quality data, and the number is red.
  3. 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.

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

  1. Create a new layout resource file and name it list_item_sleep_night.
  2. 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>
  1. 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

  1. Open SleepNightAdapter.kt.
  2. Make a class inside the SleepNightAdapter called ViewHolder and make it extend RecyclerView.ViewHolder.
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. Inside ViewHolder, get references to the views. You need a reference to the views that this ViewHolder will update. Every time you bind this ViewHolder, 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

  1. In the SleepNightAdapter definition, instead of TextItemViewHolder, use the SleepNightAdapter.ViewHolder that you just created.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

Update onCreateViewHolder():

  1. Change the signature of onCreateViewHolder() to return the ViewHolder.
  2. Change the layout inflator to use the correct layout resource, list_item_sleep_night.
  3. Remove the cast to TextView.
  4. Instead of returning a TextItemViewHolder, return a ViewHolder.

    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():

  1. Change the signature of onBindViewHolder() so that the holder parameter is a ViewHolder instead of a TextItemViewHolder.
  2. Inside onBindViewHolder(), delete all the code, except for the definition of item.
  3. Define a val res that holds a reference to the resources for this view.
val res = holder.itemView.context.resources
  1. 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)
  1. This gives an error, because convertDurationToFormatted() needs to be defined. Open Util.kt and uncomment the code and associated imports for it. (Select Code > Comment with Line comments.)
  2. Back in onBindViewHolder(), use convertNumericQualityToString() to set the quality.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. You may need to manually import these functions.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. 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
})
  1. Here is the finished updated onBindViewHolder() function, setting all the data for the ViewHolder:
   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
        })
    }
  1. 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.

  1. In SleepNightAdapter, in onBindViewHolder(), select everything except the statement to declare the variable item.
  2. Right-click, then select Refactor > Extract > Function.
  3. 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
        })
    }
  1. Put the cursor on the word holder of the holder parameter of bind(). Press Alt+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) {...}
  1. Cut and paste the bind() function into the ViewHolder.
  2. Make bind() public.
  3. Import bind() into the adapter, if necessary.
  4. Because it's now in the ViewHolder, you can remove the ViewHolder part of the signature. Here is the final code for the bind() function in the ViewHolder 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.

  1. In onCreateViewHolder(), select all the code in the body of the function.
  2. Right-click, then select Refactor > Extract > Function.
  3. Name the function from and accept the suggested parameters. Click OK.
  4. Put the cursor on the function name from. Press Alt+Enter (Option+Enter on a Mac) to open the intention menu.
  5. Select Move to companion object. The from() function needs to be in a companion object so it can be called on the ViewHolder class, not called on a ViewHolder instance.
  6. Move the companion object into the ViewHolder class.
  7. Make from() public.
  8. In onCreateViewHolder(), change the return statement to return the result of calling from() in the ViewHolder 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)
   }
}
  1. Change the signature of the ViewHolder class so that the constructor is private. Because from() is now a method that returns a new ViewHolder instance, there's no reason for anyone to call the constructor of ViewHolder anymore.
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. 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 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, the Adapter needs to inform the RecyclerView when that data changes. Use notifyDataSetChanged()to notify the Adapter 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: 7.2: DiffUtil and data binding with RecyclerView