ExpandableRecyclerView: Expandable Items With Room Database in Kotlin
Tags: room, android, kotlin, recyclerview
Published: 2020-05-01 17:39:38

Sometimes we want to display an extra content about the list items  which does not necessarily need a separate screen usually called the detail screen,this is where the Expandable RecyclerView comes in, We are going to be learning how to create an expandable recyclerview using the [`THOUGHBOT`](https://github.com/thoughtbot/expandable-recycler-view) expandable recyclerview library. We will also be fetching our items from local database using  [`Room Persistence Library`](https://developer.android.com/topic/libraries/architecture/room)  which is part of the [`Android Architecture Components`](https://developer.android.com/topic/libraries/architecture)

We will be displaying the list of continents and some of the countries under them from local database which is added only once when the database is created, the end result should look like the below images.

![ParentView](https://dev-to-uploads.s3.amazonaws.com/i/lh4x1ebg3bbqps9iame8.png)
![Parent with child](https://dev-to-uploads.s3.amazonaws.com/i/yt788kzthmxnma83eudj.png)

After you create a new project with empty activity add the following dependencies to your app level build.gradle

   ``` 
// Room components
    implementation "androidx.room:room-runtime:$rootProject.roomVersion"
    kapt "androidx.room:room-compiler:$rootProject.roomVersion"
    implementation "androidx.room:room-ktx:$rootProject.roomVersion"
    androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

//Gson
    implementation 'com.google.code.gson:gson:2.8.6'

// Lifecycle components
    implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
    kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"

// Kotlin components
 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
 api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

// Material design
    implementation "com.google.android.material:material:$rootProject.materialVersion"

//Expandable
    implementation 'com.thoughtbot:expandablerecyclerview:1.3'
    implementation 'com.thoughtbot:expandablecheckrecyclerview:1.4'
```
Also add the versions to the project level build.gradle

```
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}
```
make sure the following plugins are present at the top of the app level build. gradle 
```
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

apply plugin: 'kotlin-kapt'
```
There are three major classes required while working with Room,the [`Entity`](https://developer.android.com/training/data-storage/room/defining-data) class which represents a table in the database,the [`DAO`](https://developer.android.com/training/data-storage/room/accessing-data) class just as its name implies a data access object which contains the methods used for accessing the database, the [`database`](https://developer.android.com/reference/androidx/room/Database) class.

ContinentEntity.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.data.model

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters

@Entity(tableName = "continent-table")
@TypeConverters(ContinentConverter::class)
data class ContinentEntity
 (@PrimaryKey @ColumnInfo(name = "continent") 
val continentName: String,  val countrys: List<Country>
)

```
This class has the @Entity annotation passing the table name to be created into its parameter which is optional if you don't want the class name to be used as the table name, @ColumnInfo tells the database to use continent as the column name instead of continentName variable and @PrimaryKey which all tables must have. Also notice the @TypeConverters which is the annotation that tells room to convert the List with the ContinentConverter class

ContinentConverter.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.data.model

import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
import java.util.*


class ContinentConverter {
companion object {
        var gson: Gson = Gson()

  @TypeConverter
  @JvmStatic
  fun stringToSomeObjectList(data: String?): List<Country> {
  val listType: Type =
  object : TypeToken<List<Country?>?>() {}.getType()
  return gson.fromJson(data, listType)
        }

   @TypeConverter
   @JvmStatic
   fun someObjectListToString(someObjects: List<Country>?): String {
   return gson.toJson(someObjects)
        }
    }
}
```
This is the converter class that has @TypeConverter on each method performing the conversion with Gson library

ContinentDao.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.data.db

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import 
com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continents


@Dao
interface ContinentDao {
    @Query("SELECT * from `continent-table` ORDER BY continent ASC")
    fun getAllContinent(): LiveData<List<ContinentEntity>>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(continent: ContinentEntity)
}
```
This is the dao interface for accessing the database, getAllContinent method has the @Query annotation that fetches all data in ascending order, it returns a [`LiveData`](https://developer.android.com/topic/libraries/architecture/livedata.html) which helps keep the data updated and automatically runs the operation asynchronously on background thread. The insert method has the @Insert annotation that inserts data taking care of conflicts that might occur, it uses the suspend function to indicate that the method requires time to execute since we don't want to block the main thread. 

ContinentDatabase.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.data.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import com.developer.kulloveth.expandablelistsamplewithroom.data.DataGenerator
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@Database(entities = [ContinentEntity::class], version = 1, exportSchema = false)

abstract class ContinentDatabase : RoomDatabase() {

    abstract fun continentDao(): ContinentDao


    companion object {
        @Volatile
        private var INSTANCE: ContinentDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope): ContinentDatabase {

            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildDatabase(context, scope).also {
                    INSTANCE = it
                }
            }
        }

 private fun buildDatabase(context: Context, scope: CoroutineScope): ContinentDatabase {
   return Room.databaseBuilder(context, ContinentDatabase::class.java, "place_db")
 .addCallback(object : RoomDatabase.Callback() 
 override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)

               scope.launch {
                 INSTANCE?.let {
                      for (continent: ContinentEntity in DataGenerator.getContinents()) {
            it.continentDao().insert(
                  ContinentEntity(
                           continent.continentName,
                                            continent.countrys
                                        ) )
                                }}}}}).build()
}}}
```
This is the database class which must be an abstract class and must contain an abstract method representing the dao interface class,it has @Database with the its entity, version and set export-schema to false since we are not exporting the database into a folder. The getDatabase method is a singleton which ensures that only one instance of the database is open at any time, we also added a roomCallback to insert the data only once when the room is created using its onCreate method. notice that the insert method is called within a coroutine scope since its a suspend function to ensure the operation is performed on the background thread.

DataGenerator.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.data

import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Country

class DataGenerator {

    companion object {
        fun getContinents(): List<ContinentEntity> {
            return listOf(
    ContinentEntity("Europe", europeCountrys()),
    ContinentEntity("Africa", africaCountrys()),
    ContinentEntity("Asia", asiaCountrys()),
    ContinentEntity("North America", northAmericaCountrys()),
    ContinentEntity("South America", southAmericaCountrys()),
    ContinentEntity("Antarctica", antarcticaCountrys()),
    ContinentEntity("Oceania", oceaniaCountrys())
            )
        }

        fun europeCountrys(): List<Country> {
            return listOf(
                Country("Germany"),
                Country("Italy"),
                Country("France"),
                Country("United Kingdom"),
                Country("NertherLand")
            )
        }

        fun africaCountrys(): List<Country> {
            return listOf(
                Country("South Africa"),
                Country("Nigeria"),
                Country("Kenya"),
                Country("Ghana"),
                Country("Ethiopia")
            )

        }

        fun asiaCountrys(): List<Country> {
            return listOf(
                Country("Japan"),
                Country("India"),
                Country("Indonesi"),
                Country("China"),
                Country("Thailand")
            )
        }

        fun northAmericaCountrys(): List<Country> {
            return listOf(
                Country("United States"),
                Country("Mexico"),
                Country("Cuba"),
                Country("Green Land")
            )
        }


        fun southAmericaCountrys(): List<Country> {
            return listOf(
                Country("Brazil"),
                Country("Argentina"),
                Country("Columbia"),
                Country("Peru"),
                Country("Chile")
            )}

       fun antarcticaCountrys(): List<Country> {
            return listOf(
                Country("Esperenza Base"),
                Country("Villa az Estrellaz"),
                Country("General Bernando O'Higging"),
                Country("Bellgrano II base"),
                Country("Carlini Base") )}

        fun oceaniaCountrys(): List<Country> {
            return listOf(
                Country("Australia"),
                Country("Newzeland"),
                Country("Fiji"),
                Country("Samao"),
                Country("Federated States")
            )}}}
```

Next we are going to be creating the Adapter, its data class,observe the data we added to Room and setup the recyclerView.

Continent.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.data.model
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup
data class Continent(
    val continentName: String, val countries: List<Country>
):  ExpandableGroup<Country>(continentName, countries)
```
Country.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.data.model

import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Country(val countryName: String) : Parcelable
```
Continent class is the Parent class to be used with the adapter which will be extending the ExpandableGroup passing the child class Country

continent_layout.xml

```
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="0dp"
    app:cardUseCompatPadding="true">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <ImageView
            android:id="@+id/arrow"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_marginEnd="16dp"
            android:src="@drawable/ic_arrow_drop_down_black_24dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/continent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="?listPreferredItemPaddingLeft"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>
```
countrys_layout.xml

```
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:padding="0dp">

    <TextView
        android:id="@+id/countryName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        tools:text="Niger" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@android:color/black" />

</androidx.cardview.widget.CardView>
```
The above layouts are the parent and child layout for the items to be referenced in their respective viewholder

MainViewHolder.kt

```
package com.developer.kulloveth.expandablelistsamplewithroom.data

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.developer.kulloveth.expandablelistsamplewithroom.R
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continent
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Country
import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder
import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder


class CountryViewHolder(itemView: View) : ChildViewHolder(itemView) {
    val countryName = itemView.findViewById<TextView>(R.id.countryName)

    fun bind(country: Country) {
        countryName.text = country.countryName
    }
}

class ContinentViewHolder(itemView: View) : GroupViewHolder(itemView) {
    val continentName = itemView.findViewById<TextView>(R.id.continent)
    val arrow = itemView.findViewById<ImageView>(R.id.arrow)

    fun bind(continent: Continent) {
        continentName.text = continent.continentName
    }

}
```
The MainViewHolder is a kotlin file containing both the parent viewholder and child viewholder

ContinentAdapter.kt

```
package com.developer.kulloveth.expandablelistsamplewithroom.data.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import com.developer.kulloveth.expandablelistsamplewithroom.R
import com.developer.kulloveth.expandablelistsamplewithroom.data.ContinentViewHolder
import com.developer.kulloveth.expandablelistsamplewithroom.data.CountryViewHolder
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continent
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Country
import com.thoughtbot.expandablerecyclerview.ExpandableRecyclerViewAdapter
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup

class ContinentAdapter(groups: List<ExpandableGroup<*>>?) :
    ExpandableRecyclerViewAdapter<ContinentViewHolder, CountryViewHolder>(
        groups
    ) {


    override fun onCreateGroupViewHolder(parent: ViewGroup?, viewType: Int): ContinentViewHolder {
        val itemView =
            LayoutInflater.from(parent?.context).inflate(R.layout.continent_layout, parent, false)
        return ContinentViewHolder(itemView)
    }

    override fun onCreateChildViewHolder(parent: ViewGroup?, viewType: Int): CountryViewHolder {
        val itemView =
            LayoutInflater.from(parent?.context).inflate(R.layout.countrys_layout, parent, false)
        return CountryViewHolder(itemView)
    }

    override fun onBindChildViewHolder(
        holder: CountryViewHolder?,
        flatPosition: Int,
        group: ExpandableGroup<*>?,
        childIndex: Int
    ) {
        val country: Country = group?.items?.get(childIndex) as Country
        holder?.bind(country)
    }

    override fun onBindGroupViewHolder(
        holder: ContinentViewHolder?,
        flatPosition: Int,
        group: ExpandableGroup<*>?
    ) {
        val continent: Continent = group as Continent
        holder?.bind(continent)
    }
}
```
The adapter class accepts a List of ExpandableGroup type extending ExpandableAdapter with the parent and child viewholder respectively,Both parent and child has its own oncreateViewHolder and onBindViewHolder

Repository.kt

```
package com.developer.kulloveth.expandablelistsamplewithroom.data.model

import androidx.lifecycle.LiveData
import com.developer.kulloveth.expandablelistsamplewithroom.data.db.ContinentDao

class Repository(continentDao: ContinentDao) {

    val allContinents: LiveData<List<ContinentEntity>> = continentDao.getAllContinent()


}
```
MainActivityViewModel.kt

```
package com.developer.kulloveth.expandablelistsamplewithroom.ui

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.developer.kulloveth.expandablelistsamplewithroom.data.db.ContinentDatabase
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Repository

class MainActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val repository: Repository
    val continents: LiveData<List<ContinentEntity>>
    init {
        val continentDao = ContinentDatabase.getDatabase(application, viewModelScope).continentDao()
        repository = Repository(continentDao)
        continents = repository.allContinents
    }
}
```
The repository pattern helps to separate business logic from the UI logic which is most useful when you are fetching data from different sources. The viewmodel class provides data to the UI and also survives configuration changes

activity_main.xml
```
<?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=".data.ui.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:id="@+id/rvConinent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
MainActivity.kt
```
package com.developer.kulloveth.expandablelistsamplewithroom.ui

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.developer.kulloveth.expandablelistsamplewithroom.R
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continent
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainActivityViewModel
    val continents = ArrayList<Continent>()
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
   viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]

    viewModel.continents.observe(this, Observer {

   for (continentEntity: ContinentEntity in it) {

    val continent = Continent(continentEntity.continentName, continentEntity.countrys)
                continents.add(continent)}
            val adapter = ContinentAdapter(continents)
            rvConinent.apply {
                layoutManager = LinearLayoutManager(this@MainActivity)
                rvConinent.adapter = adapter
            } })}}


```
finally the main layout and its activity that Observes the data from the MainActivityViewModel, adds to a new list and displays on the recyclerView.

![Final Result](https://dev-to-uploads.s3.amazonaws.com/i/bm7rnj0ne29bzbtpqhpa.gif)

kindly leave your Feedback and suggestions  in the comment below [`TWITTER`](https://www.twitter.com/kulloveth)
{% github kulloveth/ExpandableRecyclerViewSampleWithRoom %}