How to add RecyclerView inside RecyclerView using Kotlin

How to add RecyclerView inside RecyclerView using Kotlin

Today, I’m going to show you how to add a RecyclerView inside another RecyclerView.

In this example that I’m about to show you, we have categories, and inside the categories, we have a different number of subcategories.

Each subcategory has a RecyclerView and shows the items horizontally.

Every item has an ImageView to show the color and a TextView for the name of the color.

Prepare the Data and Models

Before we start, create an empty Kotlin file and name it Colors (Colors.kt) and paste the following data inside:

data class Colors(
    var objectsArray: ArrayList<Vertical_RVModel> = arrayListOf(
        Vertical_RVModel(
            "Category #1", arrayListOf("SubCategory #1.1", "SubCategory #1.2"),
            arrayListOf(
                // SubCategory #1.1
                arrayListOf(
                    Horizontal_RVModel("#DA70D6", "Orchid"),
                    Horizontal_RVModel("#FA8072", "Salmon"),
                    Horizontal_RVModel("#FDF5E6", "Old Lace"),
                    Horizontal_RVModel("#00FFFF", "Aqua"),
                    Horizontal_RVModel("#2E8B57", "Sea Green")
                ),
                // SubCategory #1.2
                arrayListOf(
                    Horizontal_RVModel("#2F4F4F", "Dark Slate Gray"),
                    Horizontal_RVModel("#F0FFF0", "Honeydew"),
                    Horizontal_RVModel("#DCDCDC", "Gainsboro")
                )
            )
        ),
        Vertical_RVModel(
            "Category #2", arrayListOf("SubCategory #2.1"),
            arrayListOf(
                // SubCategory #2.1
                arrayListOf(
                    Horizontal_RVModel("#FFE4B5", "Moccasin"),
                    Horizontal_RVModel("#AFEEEE", "Pale Turquoise"),
                    Horizontal_RVModel("#9400D3", "Dark Violet"),
                    Horizontal_RVModel("#3CB371", "Medium Sea Green")
                )
            )
        ),
        Vertical_RVModel(
            "Category #3", arrayListOf("SubCategory #3.1", "SubCategory #3.2"),
            arrayListOf(
                // SubCategory #3.1
                arrayListOf(
                    Horizontal_RVModel("#FF6347", "Tomato"),
                    Horizontal_RVModel("#4682B4", "Steel Blue"),
                    Horizontal_RVModel("#778899", "Light Slate Gray"),
                    Horizontal_RVModel("#191970", "Midnight Blue"),
                    Horizontal_RVModel("#A52A2A", "Brown")
                ),
                // SubCategory #3.2
                arrayListOf(
                    Horizontal_RVModel("#FFF8DC", "Cornsilk"),
                    Horizontal_RVModel("#FF00FF", "Magenta"),
                    Horizontal_RVModel("#7CFC00", "Lawn Green"),
                    Horizontal_RVModel("#000000", "Black"),
                    Horizontal_RVModel("#00BFFF", "Deep Sky Blue"),
                    Horizontal_RVModel("#6495ED", "Cornflower Blue"),
                    Horizontal_RVModel("#FF8C00", "Dark Orange"),
                    Horizontal_RVModel("#20B2AA", "Light Sea Green"),
                    Horizontal_RVModel("#FFC0CB", "Pink")
                )
            )
        ),
        Vertical_RVModel(
            "Category #4", arrayListOf("SubCategory #4.1", "SubCategory #4.2"),
            arrayListOf(
                // SubCategory #4.1
                arrayListOf(
                    Horizontal_RVModel("#DDA0DD", "Plum"),
                    Horizontal_RVModel("#FFF5EE", "Seashell"),
                    Horizontal_RVModel("#FFDEAD", "Navajo White"),
                    Horizontal_RVModel("#00FF00", "Lime"),
                    Horizontal_RVModel("#F0E68C", "Khaki")
                ),
                // SubCategory #4.2
                arrayListOf(
                    Horizontal_RVModel("#FAEBD7", "Antique White"),
                    Horizontal_RVModel("#C71585", "Medium Violet Red"),
                    Horizontal_RVModel("#6B8E23", "Olive Drab"),
                    Horizontal_RVModel("#FF4500", "Orange Red"),
                    Horizontal_RVModel("#FFF0F5", "Lavender Blush")
                )
            )
        ),
        Vertical_RVModel(
            "Category #5", arrayListOf("SubCategory #5.1", "SubCategory #5.2"),
            arrayListOf(
                // SubCategory #5.1
                arrayListOf(
                    Horizontal_RVModel("#9966CC", "Amethyst")
                ),
                // SubCategory #5.2
                arrayListOf(
                    Horizontal_RVModel("#7B68EE", "Medium Slate Blue"),
                    Horizontal_RVModel("#800000", "Maroon"),
                    Horizontal_RVModel("#FFA07A", "Light Salmon"),
                    Horizontal_RVModel("#E6E6FA", "Lavender"),
                    Horizontal_RVModel("#FFE4C4", "Bisque")
                )
            )
        )
    )
)

In Vertical RecyclerView there are 3 things you need:

  • Category Name
  • SubCategory Name
  • List of Colors

Create a new Kotlin file, name it Vertical_RVModel, and paste inside the following code:

data class Vertical_RVModel(
    val category: String,
    val subcategory: ArrayList<String>,
    val colors: ArrayList<ArrayList<Horizontal_RVModel>>
)

The List of Colors has another model, which contains for each item:

  • Hex code for Color
  • Color name

Create a new Kotlin file, name it Horizontal_RVModel, and paste inside the following code:

data class Horizontal_RVModel (
    val color: String,
    val name: String
)

Creating the Vertical RecyclerView

Go to your Activity‘s layout, in this example is activity_main.xml and add a RecyclerView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimaryDark"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/vertical_rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />


</RelativeLayout>

If you don’t have added the RecyclerView dependency in your project, go to build.gradle of your app and add the following line to your dependencies:

dependencies {

    // ...

    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    
    // ...
    
}

After you added the RecyclerView, create a layout for the items.

Each item has the TextView for the Subcategory, and the Horizontal RecyclerView that we’ll add later.

Create a .xml file inside the layout folder (res > layout), name it vertical_item and paste the following code:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/subcategory_text"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:background="@color/colorPrimaryDark"
        android:gravity="center_vertical"
        android:paddingStart="16dp"
        android:paddingLeft="16dp"
        android:paddingEnd="0dp"
        android:paddingRight="0dp"
        android:text="SubCategory #1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:textStyle="bold" />
    
</LinearLayout>

In this tutorial, we’re going to use Sections, but RecyclerView doesn’t have this feature build-in.

So we have to rely on the 3rd party library called SectionedRecyclerViewAdapter for this thing, which will make the process painless.

Go to your dependencies again and add:

dependencies {

    // ...

    implementation 'io.github.luizgrp.sectionedrecyclerviewadapter:sectionedrecyclerviewadapter:3.1.0'
    
    // ...
    
}

Now let’s make the layout for the header for each section.

Create a .xml file inside the layout folder (res > layout), name it vertical_header, and paste inside the following code:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="35dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/vertical_category_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        android:gravity="center_vertical"
        android:paddingStart="10dp"
        android:paddingLeft="10dp"
        android:paddingEnd="10dp"
        android:paddingRight="10dp"
        android:text="Category #1"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:textStyle="bold" />

</LinearLayout>

It’s time now to make the adapter for this RecyclerView.

Create a new Kotlin file and name it Vertical_RVAdapter.

The adapter will get as parameters the category name, the subcategories for this section, and the list of colors for each subcategory.

The SectionedRecyclerViewAdapter we added before, will give as the ability to set a layout for the header and item for each section.

Also, we’ll be able to create the ViewHolders and BindViewHolders for them.

class Vertical_RVAdapter(private val category: String, private val subcategory: ArrayList<String>, private val colors: ArrayList<ArrayList<Horizontal_RVModel>>) : Section(SectionParameters.builder().itemResourceId(R.layout.vertical_item).headerResourceId(R.layout.vertical_header).build()) {

    // Size of Subcategories in each Section
    override fun getContentItemsTotal(): Int {
        return subcategory.size
    }

    class SectionViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView)

    class ItemViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView)

    override fun getHeaderViewHolder(view: View): RecyclerView.ViewHolder {
        return SectionViewHolder(view)
    }

    override fun getItemViewHolder(view: View): RecyclerView.ViewHolder {
        return ItemViewHolder(view)
    }


    override fun onBindHeaderViewHolder(holder: RecyclerView.ViewHolder) {
        holder.itemView.vertical_category_text.setTextColor(Color.WHITE)
        holder.itemView.vertical_category_text.text = category
    }

    override fun onBindItemViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val itemHolder = holder as ItemViewHolder
        // Subcategory
        itemHolder.itemView.subcategory_text.text = subcategory[position]
    }
}

Creating the Horizontal RecyclerView

Go to the vertical_item.xml file you created before and under the TextView for the Subcategory, add a RecyclerView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/subcategory_text"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:background="@color/colorPrimaryDark"
        android:gravity="center_vertical"
        android:paddingStart="16dp"
        android:paddingLeft="16dp"
        android:paddingEnd="0dp"
        android:paddingRight="0dp"
        android:text="SubCategory #1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:textStyle="bold" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/horizontal_rv"
        android:layout_width="match_parent"
        android:layout_height="185dp"
        android:background="@color/colorPrimaryDark"
        android:clipToPadding="false"
        android:paddingStart="4dp"
        android:paddingLeft="4dp"
        android:paddingEnd="4dp"
        android:paddingRight="4dp"/>

</LinearLayout>

As you see, we added 4dp padding in the left and right side of the RecyclerView, together with the android:clipToPadding=”false” allows us to have space at the beginning and end of the RecyclerView.

Create a new .xml file inside the layout folder (res > layout), name it horizontal_item, and paste the following code:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="180dp"
android:background="@color/colorPrimaryDark"
android:orientation="vertical">

<ImageView
    android:id="@+id/item_image"
    android:layout_width="150dp"
    android:layout_height="150dp"
    android:background="#FA8072"
    android:scaleType="centerCrop" />


<TextView
    android:id="@+id/item_title"
    android:layout_width="match_parent"
    android:layout_height="30dp"
    android:ellipsize="end"
    android:gravity="center"
    android:maxLines="1"
    android:text="Title"
    android:textColor="@android:color/white"
    android:textStyle="bold" />


</LinearLayout>

Now, create a new Kotlin file and name it Horizontal_RVAdapter, for the adapter of this RecyclerView.

In this adapter, we create a method setColorsData that takes the List of Colors from the Vertical_RVAdapter and updates the adapter.

Also, we create a setRowIndex method to get the number of the row inside the Horizontal RecyclerView.

class Horizontal_RVAdapter(private var mContext: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private var itemsList: ArrayList<Horizontal_RVModel> = ArrayList()
    private var mRowIndex = -1

    fun setColorsData(data: ArrayList<Horizontal_RVModel>) {
        if (itemsList != data) {
            itemsList = data
            notifyDataSetChanged()
        }
    }

    fun setRowIndex(index: Int) {
        mRowIndex = index
    }

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView =
            LayoutInflater.from(mContext).inflate(R.layout.horizontal_item, parent, false)
        return ItemViewHolder(itemView)
    }

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

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        holder.itemView.item_image.setBackgroundColor(Color.parseColor(itemsList[position].color))
        holder.itemView.item_title.text = itemsList[position].name
        holder.itemView.setOnClickListener {
            Log.d(
                "TAG",
                "You pressed the item in the category row $mRowIndex and position $position"
            )
        }

    }

}

Connecting the RecyclerViews

Now we’ve done both RecyclerViews it’s time to connect the Horizontal with the Vertical RecyclerView.

Go to your Vertical_RVAdapter and initialize the Horizontal RecyclerView in the ItemViewHolder and on onBindItemViewHolder pass the list of colors and the number of the current row:

class Vertical_RVAdapter(private val category: String, private val subcategory: ArrayList<String>, private val colors: ArrayList<ArrayList<Horizontal_RVModel>>) : Section(SectionParameters.builder().itemResourceId(R.layout.vertical_item).headerResourceId(R.layout.vertical_header).build()) {

    // ...

    class ItemViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) {
        private val horizontalRecyclerView: RecyclerView
        val horizontalAdapter: Horizontal_RVAdapter

        init {
            val context = itemView.context
            horizontalRecyclerView = itemView.findViewById(R.id.horizontal_rv)
            horizontalRecyclerView?.layoutManager =
                LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            horizontalRecyclerView?.addItemDecoration(
                EqualSpacingItemDecoration(
                    8,
                    EqualSpacingItemDecoration.HORIZONTAL
                )
            )
            horizontalAdapter = Horizontal_RVAdapter(context)
            horizontalRecyclerView.adapter = horizontalAdapter
        }
    }

    // ...

    override fun onBindItemViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val itemHolder = holder as ItemViewHolder
        // ...  
      
        // Color items
        itemHolder.horizontalAdapter.setColorsData(colors[position])
        // Pass the current row
        itemHolder.horizontalAdapter.setRowIndex(position)
    }
}

In this example, I’m using the class EqualSpacingItemDecoration that makes equal spacing between items.

If you want to use it on your project too, create a new Kotlin file, name it EqualSpacingItemDecoration, and paste the following code inside:

class EqualSpacingItemDecoration(
    private val spacing: Int,
    private var displayMode: Int
) :
    ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildViewHolder(view).adapterPosition
        val itemCount = state.itemCount
        val layoutManager = parent.layoutManager
        setSpacingForDirection(outRect, layoutManager, position, itemCount)
    }

    private fun setSpacingForDirection(
        outRect: Rect,
        layoutManager: RecyclerView.LayoutManager?,
        position: Int,
        itemCount: Int
    ) { // Resolve display mode automatically
        if (displayMode == -1) {
            displayMode = resolveDisplayMode(layoutManager)
        }
        when (displayMode) {
            HORIZONTAL -> {
                outRect.left = spacing
                outRect.right = if (position == itemCount - 1) spacing else 0
                outRect.top = spacing
                outRect.bottom = spacing
            }
            VERTICAL -> {
                outRect.left = spacing
                outRect.right = spacing
                outRect.top = spacing
                outRect.bottom = if (position == itemCount - 1) spacing else 0
            }
            GRID -> if (layoutManager is GridLayoutManager) {
                val cols = layoutManager.spanCount
                val rows = itemCount / cols
                outRect.left = spacing
                outRect.right = if (position % cols == cols - 1) spacing else 0
                outRect.top = spacing
                outRect.bottom = if (position / cols == rows - 1) spacing else 0
            }
        }
    }

    private fun resolveDisplayMode(layoutManager: RecyclerView.LayoutManager?): Int {
        if (layoutManager is GridLayoutManager) return GRID
        return if (layoutManager!!.canScrollHorizontally()) HORIZONTAL else VERTICAL
    }

    companion object {
        const val HORIZONTAL = 0
        private const val VERTICAL = 1
        private const val GRID = 2
    }

}

Final Step: Setting up Vertical RecyclerView and Adding Data

Go to your Activity‘s file, set RecyclerView‘s layout to Vertical, and pass the SectionedRecyclerViewAdapter() to RecyclerView’s(vertical_rv) adapter.

class MainActivity : AppCompatActivity() {

    private val sectionAdapter = SectionedRecyclerViewAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        vertical_rv.layoutManager = layoutManager
        vertical_rv.adapter = sectionAdapter
        
        // ...
    }
}

Last, create an AsyncTask class, add the data, and update the adapter.


class MainActivity : AppCompatActivity() {
  
    // ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        //...
      
        FetchDemoData(this, vertical_rv, sectionAdapter).execute()
    }


    private class FetchDemoData
    internal constructor(mContext: MainActivity, verticalRecyclerView: RecyclerView, sectionAdapter: SectionedRecyclerViewAdapter) :
        AsyncTask<Void, Void, Void>() {

        private val activityReference: WeakReference<MainActivity> = WeakReference(mContext)

        var verticalRv = verticalRecyclerView
        var secAdapter = sectionAdapter

        val colors = Colors().objectsArray

        override fun doInBackground(vararg params: Void): Void? {
            for (color in colors) {
                secAdapter.addSection(
                    Vertical_RVAdapter(
                        color.category,
                        color.subcategory,
                        color.colors
                    )
                )
            }
            return null
        }

        override fun onPostExecute(result: Void?) {
            // get a reference to the activity if it is still there
            val activity = activityReference.get()
            if (activity == null || activity.isFinishing) return
            verticalRv.adapter?.notifyDataSetChanged()
        }
    }
}
You can find the final project here

Did you find this tutorial helpful?



If you have any questions, please feel free to leave a comment below

Subscribe
Notify of
guest
3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Android

Hello, It’s working and thank you (;

But I want to replace the colors by some Images I changed the values of color from String to Int but I have a problem in the Horizontal_RvAdapter

Android

OHH, I am sorry I found the problem. And thank you so much, it’s working