top of page

RecyclerView - Kotlin

Displaying a list of items to a user is one of the most common elements in an application. You can accomplish this task in a few ways, but the best way would be using a RecyclerView. In this article, I’ll show you how to create a custom adapter, style the displayed rows, and explain what the RecyclerView is doing in the background. The data used in this article will be related to the 20 amino acids. For those unaware, these are unique molecules the body utilizes to create proteins.


YouTube Video


GitHub

Starter Code (if you want to follow along)


Getting Started

When creating a RecyclerView, determining how/where the data will come from is the first thing to do. Will it be from a local database, your backend server, or a third-party API? To keep this article simple, I created an object called "fakeRepository," which stores a mapping of the molecules' full names, three/one-letter abbreviations, and their structures. The structure of each molecule is a drawable resource, and its corresponding id is the stored value. I also created an enum class serving as the molecule id to distinguish between specific molecule properties.

enum class MoleculeId {
    ALANINE, ARGININE, ASPARAGINE, ASPARTIC_ACID, CYSTEINE, GLUTAMIC_ACID,
    GLUTAMINE, GLYCINE, HISTIDINE, ISOLEUCINE, LEUCINE, LYSINE, 
    METHIONINE, PHENYLALANINE, PROLINE, SERINE, THREONINE, TRYPTOPHAN, 
    TYROSINE, VALINE
}
object fakeRepository {
    val moleculeName = mapOf(
        MoleculeId.ALANINE to "Alanine",
        MoleculeId.ARGININE to "Arginine",
        ...
        MoleculeId.VALINE to "Valine"
    )

    val molecule3LetterAbbr = mapOf(
        MoleculeId.ALANINE to "Ala",
        MoleculeId.ARGININE to "Arg",
        ...
        MoleculeId.VALINE to "Val"
    )

    val molecule1LetterAbbr = mapOf(
        MoleculeId.ALANINE to "A",
        MoleculeId.ARGININE to "R",
        ...
        MoleculeId.VALINE to "V"
    )

    val structures = mapOf(
        MoleculeId.ALANINE to R.drawable.ic_alanine,
        MoleculeId.ARGININE to R.drawable.ic_arginine,
        ...
        MoleculeId.VALINE to R.drawable.valine
    )
}

I want to mention that I also defined two colors within the colors.xml file. The colors I added are listed below

<color name="blueBlack">#111f28</color>
<color name="tanAccent">#e3cdb3</color>

The Layout Files

Now that you have your data, let's add a RecyclerView to our Activity/Fragment. The easiest way to do this is to head over to your layout resources file, click the "design" tab, and then from the pallet, drag and drop the RecyclerView into the layout. The RecyclerView is located in the sections labeled "Common" and "Containers." Once you put a RecyclerView into your layout, apply constraints and give it an id.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/theBestRecyclerViewOnThePlanet"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

Once your RecyclerView has been added to your Activities/Fragments layout resource file, it’s time to create a template for the rows. To do this:

  1. Go to res > layout and right-click on the layout folder

  2. Select “new” and then “Layout Resource File”

  3. Give the file a name and click “OK”

When creating RecyclerView rows, I generally like using a CardView as the top tag, and within the CardView, I’ll place a ConstraintLayout.

<androidx.cardview.widget.CardView
    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"
    android:layout_marginBottom="10dp"
    app:cardCornerRadius="8dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/blueBlack"/>
        
</androidx.cardview.widget.CardView>

I want to point out that the CardView has a layout height of “wrap_content.” Without this, you’ll find an unusually large spacing between each item within the RecyclerView.

Contained within the ConstraintLayout are one ImageView and three TextViews.

<ImageView
    android:id="@+id/moleculeStructure"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_marginStart="5dp"
    android:padding="5dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintDimensionRatio="1:1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:srcCompat="@drawable/histidine"
    app:tint="@color/tanAccent" />

<TextView
    android:id="@+id/moleculeName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="25dp"
    android:textColor="@color/tanAccent"
    android:textSize="35sp"
    android:textStyle="bold"
    app:layout_constraintBottom_toTopOf="@+id/molecule3LetterAbbr"
    app:layout_constraintStart_toEndOf="@+id/moleculeStructure"
    app:layout_constraintTop_toTopOf="parent"
    tools:text="Histidine" />

<TextView
    android:id="@+id/molecule3LetterAbbr"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="25dp"
    android:textColor="@color/tanAccent"
    android:textSize="25sp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toEndOf="@+id/moleculeStructure"
    app:layout_constraintTop_toBottomOf="@+id/moleculeName"
    tools:text="His" />

<TextView
    android:id="@+id/molecule1LetterAbbr"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="25dp"
    android:textColor="@color/tanAccent"
    android:textSize="25sp"
    app:layout_constraintBottom_toBottomOf="@+id/molecule3LetterAbbr"
    app:layout_constraintStart_toEndOf="@+id/molecule3LetterAbbr"
    app:layout_constraintTop_toTopOf="@+id/molecule3LetterAbbr"
    tools:text="H" />

For the sake of simplicity, I've hard-coded specific values for various properties. This should not be practiced in an actual project.


Creating a Data Class

The last thing we have to do before creating our adapter is make a data class that holds the information associated with a molecule. We will then pass a list of this type to our adapter, unpack the values, and apply them to the Views within a row. To create a new data class:

  1. Go to app > java > your innermost package

  2. Right-click, select "new," and then "Kotlin Class/Folder."

  3. Select "Data Class," name it, and click "OK."

Within this data class, you need to consider the views you created within the row layout. You’ll need to create a field to hold the data assigned to each View. In my case, I have an ImageView and three TextViews contained in each row. I’ll create four values within the data class: three of type String and one of type Int annotated with @DrawableRes.

data class Molecule(
    val name: String,
    val threeLetterAbbr: String,
    val oneLetterAbbr: String,
    @DrawableRes val structure: Int
)

If you, too, have a resource id you’re storing within your data class, I highly recommend annotating the values. Providing annotations ensures you receive the desired resource type, not a plain old integer value. Here’s a list of standard resource annotations:

Annotation


@DrawableRes

Checks that a drawable resource id was assigned

@DimenRes

Checks that a dimension resource id was assigned

@ColorRes

Checks that a color resource id was assigned

@AnyRes

Checks that the assigned value was of any R resource

To read more about annotations, check out this documentation page.



Adapter

The RecyclerView adapter is arguably the most challenging part to grasp for beginners. Over the following few paragraphs, I will describe what the adapter does and the purpose of each method in detail. To start, let's create the adapter class.

  1. Go to app > java > your innermost package

  2. Right-click, select "new," and then "Kotlin Class/Folder."

  3. Select Class, name it, and click "OK."

Once you create the Class, you should have something like this.

class MyAdapter() {
}

Now that we have our base class let's add all the necessary methods, extensions, and inner class to make the adapter work correctly. Don’t worry if you don’t know what everything means; stick with me, and I’ll explain once we set up the general adapter skeleton. Let’s start by creating an inner class; I usually call it “ItemViewHolder.”

inner class ItemViewHolder() {
}

This class needs to extend “RecyclerView.ViewHolder” while simultaneously passing in a View. It should look like the following:

inner class ItemViewHolder(view: View): RecyclerView.ViewHolder(view) {
}

Now that we have our inner class go to the base class, MyAdapter, and extend RecyclerView.Adapter specifying the type as “ItemViewHolder,” your inner class. The RecyclerView.Adapter has a constructor, so we must initialize it too. Your code should look similar to the following:

class MyAdapter() : RecyclerView.Adapter<MyAdapter.ItemViewHolder>(){
    inner class ItemViewHolder(view: View): RecyclerView.ViewHolder(view){
    }
}

Since we’re extending RecyclerView.Adapter, we need to implement some methods. You should see that your base class is underlined in red. Hover over that red line, or click the red lightbulb and select "implement members." You should see a dialog appear; select all three choices and hit "OK." Android Studio will populate your class with three new methods: getItemCount, onBindViewHolder, and onCreateViewHolder. The following is the basic skeleton of a RecyclerView adapter class. Feel free to save this as a template in Android Studio.

class MyAdapter() : RecyclerView.Adapter<MyAdapter.ItemViewHolder>(){
    inner class ItemViewHolder(view: View): RecyclerView.ViewHolder(view){
    }
    
    override fun onCreateViewHolder(
        parent: ViewGroup,viewType: Int
    ) : ItemViewHolder {
        TODO("Not yet implemented")
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int){
        TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }
}

Looking at this class, it might seem a little overwhelming. To better understand the class, we must understand what the adapter is doing in the background.


Say you have a list of 100 items you need to show the user. Instead of creating 100 rows, the RecyclerView adapter will produce enough rows to fit on the screen. Say five items can fit. The adapter will then create five rows and a few additional ones to account for partially displayed rows. If you have 100 items that need to be displayed, how can only a handful of rows be enough to show every item? That's where the adapter utilizes a process called binding. In this stage, as rows are scrolled off-screen, rather than destroying and recreating a row with new values, the existing values are updated, simulating an apparent list of 100 items. With that all in mind, let's take a closer look at each method.


The getItemCount method is the easiest to understand. All this method needs to return is the total number of items in our data set. Simple Right?


Thinking back to the general background process occurring in the adapter, it must first create enough rows to fit on the screen. Creating these rows is done with the onCreateViewHolder method. In this method, we must create a view by inflating the row layout. Once all rows are created, we need a way to update their values as they come back on screen. The onBindeViewHolder takes care of this job. Here, we have access to our data set's current position and the row to be updated.


Now that we know the purpose of each overridden method, what's the purpose of our inner class? The job of the ItemViewHolder, our inner class, is to hold references to the views we inflated in the onCreateViewHolder method. Please notice how the onCreateViewHolder returns an instance of this class while the onBindViewHolder receives one. Returning an instance of ItemViewHolder in onCreateViewHolder allows the values in the views to be updated in the onBindViewHolder method.


That was a lot to take in, so here’s a quick summary of the purpose of each overridden method and the inner class.

ItemViewHolder

Holds a reference to the views inflated in onCreateViewHolder

onCreateViewHolder

Inflates the item row layout onto a view

​onBindViewHolder

Updates the values on each row

getItemCount

Returns the number of items in the data set

Let’s finish up the adapter class by implementing each of the methods and our inner class. Starting with the ItemViewHolder inner class, we need to store a reference of each view in the item row layout file. For me, I have three TextViews and an ImageView. I'll create four values that correspond to the correct type to store a reference to them within the inner class. To access these views, findViewById is called on the View passed through the arguments. This is similar to getting a reference to views in Activities and Fragments. Your inner class should look similar to this:

inner class ItemViewHolder(view: View): RecyclerView.ViewHolder(view) {

    val moleculeName: TextView = view.findViewById(R.id.moleculeName)
    val molecule3LetterAbbr: TextView =         
        view.findViewById(R.id.molecule3LetterAbbr)
    val molecule1LetterAbbr: TextView =             
        view.findViewById(R.id.molecule1LetterAbbr)
    val moleculeStructure: ImageView = 
        view.findViewById(R.id.moleculeStructure)
}

Now that the inner class has a reference to the views created from the item row layout file, let's inflate the row layout in the onCreateViewHolder.

override fun onCreateViewHolder(
    parent: ViewGroup, viewType: Int
) : ItemViewHolder {
    val inflatedView: View = LayoutInflater.from(parent.context)
        .inflate(R.layout.recycler_view_item, parent, false)

    return ItemViewHolder(inflatedView)
}

Here, the recycler_view_item is the row we created above.


With the code we’ve written so far, the adapter can create enough rows to fit on the screen, but it has no way of changing any values. Let's fix that by adding a constructor parameter to our base class so we can pass in our data. To do that, I’ll add the following to the constructor:

class MyAdapter(
    private val data: List<Molecule>
) : RecyclerView.Adapter<MyAdapter.ItemViewHolder>(){

    ...
}

We create a private value with a list of type Molecule. Earlier in the article, we created a data class to hold data for each of our rows; the Molecule type is that data class. Once you give the adapter access to the data, you can change each row's values in the onBindViewHolder method based on where the adapter is in the list.

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
    val molecule: Molecule = data[position]

    holder.moleculeName.text = molecule.name
    holder.molecule3LetterAbbr.text = molecule.threeLetterAbbr
    holder.molecule1LetterAbbr.text = molecule.oneLetterAbbr
    holder.moleculeStructure.setImageResource(molecule.structure)
}

If the above code is confusing, remember that the holder is of type ItemViewHolder which contains a reference to each of the views in the row. The position argument is the current item that’s about to be displayed. For example, if you had a list of ten items and the user scrolled to the bottom, the position would equal nine as the last row is about to be displayed. We can use that position/index to reference the correct values in the list of data and assign them to the corresponding view.


The last thing we have to do in the adapter class is tell the adapter how much data we have. In the getItemCount method, return the size of your list of data.

override fun getItemCount(): Int {
    return data.size
}

Finishing Up

The last few things we have to do is create and assign an instance of the MyAdapter class to the RecyclerView and define a layout manager. You can do that in the onCreate method of an Activity or the onViewCreated method of a Fragment. Your code should look similar to the following:

val recyclerView: RecyclerView =     
    findViewById(R.id.theBestRecyclerViewOnThePlanet)
    
recyclerView.adapter = MyAdapter(createData())
recyclerView.layoutManager = LinearLayoutManager(this)

Here, I have the RecyclerView id set to “theBestRecyclerViewOnThePlanet.” When we create an instance of the MyAdapter class, I call a function called “createData.” This function accesses my fakeRepository object to create a list containing type Molecule. The code looks like this:

/**
 * Uses the repository to collect the raw data and bundles
 * up those values into our Molecule data class, something
 * our adapter knows how to work with
 */
private fun createData(): List<Molecule> {
    //Get data from the repository
    val names = fakeRepository.moleculeName
    val threeLetterAbbrs = fakeRepository.molecule3LetterAbbr
    val oneLetterAbbrs = fakeRepository.molecule1LetterAbbr
    val structures = fakeRepository.structures

    val moleculeData = ArrayList<Molecule>()
    MoleculeId.values().forEach { moleculeId ->
        //If the Id is in all lists, add molecule to the ArrayList
        if (containsId(
                moleculeId = moleculeId,
                names, threeLetterAbbrs, oneLetterAbbrs, structures)
            ) {
            
            moleculeData.add(
                Molecule(
                    name = names[moleculeId]!!,
                    threeLetterAbbr = threeLetterAbbrs[moleculeId]!!,
                    oneLetterAbbr = oneLetterAbbrs[moleculeId]!!,
                    structure = structures[moleculeId]!!
                )
            )
        }
    }

    return moleculeData
}

/**
 * Takes in a molecule id and checks if it
 * is contained within all mappings passed
 */
private fun containsId(moleculeID: MoleculeId, vararg maps: Map<MoleculeId, Any>): Boolean {
    maps.forEach {
        if (moleculeID !in it.keys) { return false }
    }
    return true
}

Now that the adapter is assigned, we set a linear layout manager for the RecyclerView. The layout manager is responsible for positioning the items contained within the RecyclerView. You can also change the layout manager to implement row positionings, such as a grid layout. The one shown above creates your standard scrollable list. After assigning the adapter and layout manager, you should have a working RecyclerView.


Final Result


141 views0 comments

Recent Posts

See All
bottom of page