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:
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"><ImageViewandroid: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"/><TextViewandroid: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" /><Buttonandroid: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" /><TextViewandroid: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.Propertyimport com.squareup.moshi.Moshiimport com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactoryimport retrofit2.Callimport retrofit2.Retrofitimport retrofit2.converter.moshi.MoshiConverterFactoryimport retrofit2.http.GETprivate 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.modelsdata 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.myapplicationimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.Buttonimport android.widget.ImageViewimport android.widget.TextViewimport androidx.constraintlayout.widget.ConstraintLayoutimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerViewimport com.bumptech.glide.Glideimport com.example.myapplication.models.Propertyclass 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.GONEtitle.text = property.titledescription.text = property.descriptionGlide.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.myapplicationimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerViewimport androidx.swiperefreshlayout.widget.SwipeRefreshLayoutimport com.example.myapplication.models.Propertyimport com.example.myapplication.network.Apiimport retrofit2.Callimport retrofit2.Callbackimport retrofit2.Responseclass MainActivity : AppCompatActivity() {lateinit var data: MutableList<Property>private lateinit var recyclerView: RecyclerViewprivate lateinit var manager: RecyclerView.LayoutManagerprivate lateinit var myAdapter: MyAdapterprivate lateinit var swipeRefresh: SwipeRefreshLayoutoverride 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 = manageradapter = 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.SwipeRefreshLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/swipeRefresh"><androidx.recyclerview.widget.RecyclerViewandroid: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 30buildToolsVersion "30.0.2"defaultConfig {applicationId "com.example.myapplication"minSdkVersion 16targetSdkVersion 30versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility 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:
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