Close
Close full mode
logoAppDevAssist

Horizontal RecyclerView in vertical RecyclerView in Android

How to add horizontal RecyclerView in vertical RecyclerView:

This post will show how to add a horizontal RecyclerView in a vertical RecyclerView in Android using Kotlin. User can scroll the vertical RecyclerView and also the horizontal one.

We will use the same project used in the article series.

YouTube video:

I published a video on YouTube. You can watch it here:

Please do subscribe to my channel if you love this video.

Node.js Application:

Download the Node.js project from this link. You can run npm install && npm run start or yarn && yarn start to start the server on the 3000 port.

Open any browser and go to localhost:3000/horizontal to get the response array used in this example.

Response data:

You can download the Node.js project from here. If we make a get request on /horizontal, it will return the following data:

[
{
"id" : 1,
"title" : "Autumn leaves",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2020/10/03/17/40/autumn-5624139_1280.jpg"
},
{
"id" : 2,
"title" : "Alpine mountains",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2020/10/03/10/56/mountains-5623208_1280.jpg"
},
{
"id": 0,
"horizontal": true,
"data" : [
{
"id" : 1,
"title" : "Polar light",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2020/09/24/17/59/aurora-5599375_1280.jpg"
},
{
"id" : 2,
"title" : "Horse",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2020/11/24/12/34/horse-5772416_1280.jpg"
},
{
"id" : 3,
"title" : "Berries",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2020/11/24/12/23/flowering-dogwood-5772385_1280.jpg"
},
{
"id" : 4,
"title" : "Sunset",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2020/04/04/03/10/landscape-5000655_1280.jpg"
},
{
"id" : 5,
"title" : "Flowers",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2020/06/29/08/06/helichrysum-italicum-5351797_1280.jpg"
}
]
},
{
"id" : 3,
"title" : "Mountain climbing adventure",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2016/11/08/05/20/adventure-1807524_1280.jpg"
},
{
"id" : 4,
"title" : "Northen light Aurora",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2016/02/07/19/48/aurora-1185464_1280.jpg"
},
{
"id" : 5,
"title" : "Polar Bear",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididun",
"image" : "https://cdn.pixabay.com/photo/2013/10/16/14/04/polar-bear-196318_1280.jpg"
}
]

For the third element, we have a new property horizontal which is true and it returns another array of items with the data key.

Project structure:

The files of the project looks as below:

RecyclerView project structure
RecyclerView project structure

Changes to the list item layout file:

In the list item layout file, we will add one RecyclerView component. This component will show the horizontal RecyclerView.

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView android:layout_height="wrap_content"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="8dp"
app:cardElevation="8dp"
android:layout_margin="5dp">
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recyclerView"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
<androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintWidth_percent=".3"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:src="@drawable/ic_launcher_background"
/>
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintEnd_toStartOf="@id/button"
app:layout_constraintTop_toTopOf="parent"
android:textSize="25sp"
android:fontFamily="sans-serif"
android:textColor="#212121"
android:layout_marginStart="10dp"
android:text="This is a very long title and here it is"
android:layout_marginLeft="10dp" />
<Button
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/ic_baseline_delete_24"
app:layout_constraintWidth_percent=".08"
app:layout_constraintDimensionRatio="1:1"
android:layout_marginRight="5dp" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/tvTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="18sp"
android:fontFamily="sans-serif"
android:text="This is a very long title and here it is"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

Change the APIService class:

We need to change the endpoint in the APIService class. Following are the changes we need add to the APIService.kt class file:

import com.example.myapplication.models.Property
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
private const val BASE_URL = "http://10.0.2.2:3000"
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val retrofit = Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi)).baseUrl(BASE_URL).build()
interface ApiService{
+ @GET("horizontal")
fun getAllData(): Call<List<Property>>
}
object Api {
val retrofitService: ApiService by lazy{retrofit.create(ApiService::class.java)}
}

The getAllData method returns a list of Property objects. We also need to change the Property object class.

Changes to the model Property class:

We need to change the model class as below:

package com.example.myapplication.models
data class Property(val id: Int, val title: String = "", val description: String = "", val image: String = "", val horizontal: Boolean = false, val data: List<Property>? = null)

We added two new properties horizontal and data.

Changes to the adapter class:

We need to change the MyAdapter.kt class as below:

package com.example.myapplication
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.myapplication.models.Property
class MyAdapter(data: List<Property>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
private var listData: MutableList<Property> = data as MutableList<Property>
inner class MyViewHolder(val view: View): RecyclerView.ViewHolder(view){
fun bind(property: Property, index: Int){
val title = view.findViewById<TextView>(R.id.tvTitle)
val imageView = view.findViewById<ImageView>(R.id.imageView)
val description = view.findViewById<TextView>(R.id.tvDescription)
val button = view.findViewById<Button>(R.id.button)
+ val constraintLayout = view.findViewById<ConstraintLayout>(R.id.constraintLayout)
+ val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
+ constraintLayout.visibility = View.VISIBLE
+ recyclerView.visibility = View.GONE
title.text = property.title
description.text = property.description
Glide.with(view.context).load(property.image).centerCrop().into(imageView)
button.setOnClickListener{deleteItem(index)}
}
+ fun bindRecyclerView(data: List<Property>){
+ val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
+ val constraintLayout = view.findViewById<ConstraintLayout>(R.id.constraintLayout)
+
+ constraintLayout.visibility = View.GONE
+ recyclerView.visibility = View.VISIBLE
+
+ val manager : RecyclerView.LayoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, true)
+ recyclerView.apply{
+ val data = data as MutableList<Property>
+ var myAdapter = MyAdapter(data)
+ layoutManager = manager
+ adapter = myAdapter
+ }
+ }
+ }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return MyViewHolder(v)
}
override fun getItemCount(): Int {
return listData.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
+ if(listData[position].horizontal){
+ listData[position].data?.let { holder.bindRecyclerView(it)}
+ }else {
+ holder.bind(listData[position], position)
+ }
}
fun deleteItem(index: Int){
listData.removeAt(index)
notifyDataSetChanged()
}
}
  • By default, the constraint layout is hidden and the RecyclerView is visible.
  • Inside onBindViewHolder, we are checking the horizontal property of the data. Based on its value, we are binding different data.
  • The bindRecyclerView method is used to bind the horizontal RecyclerView. It changes the visibility of the RecyclerView and the ConstraintLayout and shows a horizontal RecyclerView.

Changes to the MainActivity:

We don't need any change to the MainActivity.kt file:

package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.example.myapplication.models.Property
import com.example.myapplication.network.Api
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainActivity : AppCompatActivity() {
lateinit var data: MutableList<Property>
private lateinit var recyclerView: RecyclerView
private lateinit var manager: RecyclerView.LayoutManager
private lateinit var myAdapter: MyAdapter
private lateinit var swipeRefresh: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
manager = LinearLayoutManager(this)
swipeRefresh = findViewById(R.id.swipeRefresh)
swipeRefresh.setOnRefreshListener {
getAllData()
}
getAllData()
}
fun getAllData(){
Api.retrofitService.getAllData().enqueue(object: Callback<List<Property>>{
override fun onResponse(
call: Call<List<Property>>,
response: Response<List<Property>>
) {
if(swipeRefresh.isRefreshing){
swipeRefresh.isRefreshing = false
}
if(response.isSuccessful){
recyclerView = findViewById<RecyclerView>(R.id.recycler_view).apply{
data = response.body() as MutableList<Property>
myAdapter = MyAdapter(data)
layoutManager = manager
adapter = myAdapter
}
}
}
override fun onFailure(call: Call<List<Property>>, t: Throwable) {
t.printStackTrace()
}
})
}
}

Similarly, the activity_main.xml is not changed:

<?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="match_parent"
tools:context=".MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/swipeRefresh">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="1dp"
android:layout_marginLeft="1dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

The build.gradle file changes are:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.squareup.moshi:moshi-kotlin:1.11.0"
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
}

Output:

If you run the program, it will give output as below:

RecyclerView Horizontal example
RecyclerView Horizontal example

Github:

The code is available on Github. Please use the tut-horizontal-in-vertical tag to get the code explained on this tutorial.

git clone https://github.com/AppDevAssist/recyclerview-kotlin && git checkout tags/tut-horizontal-in-vertical

Subscribe to our Newsletter

Previous
How to add pull to refresh to a RecyclerView in Android Kotlin
Next
How to add a shimmer view to a RecyclerView in Android(Kotlin)