How to create an Expandable Item in Android RecyclerView?
Tags: android, recyclerview, kotlin, tutorial
Published: 2022-06-01 07:16:09

I've always had this question in my earlier days as an Android Developer. Every time I had to develop a screen which required some form of expandable Recycler View, I'd be lost. However, I think I've come up with a very efficient solution to build this. 

And I thought this would be a great topic for my First ever blog. So let's get into it. 

I've created a very simple project to demonstrate this. 

![Demo Gif](https://github.com/rdias002/expandable-recyclerview-demo/blob/af30a724f0fb01540c6e7c508e6bd901faeab2d3/screenshots/demo.gif?raw=true)

We start with a simple RecyclerView in `MainActivity.kt`.
```kotlin
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:listitem="@layout/list_item_view"
    tools:itemCount="5"/>
```

The `list_item_view.xml` will contain the views to display when the item is expanded and collapsed. 
```kotlin
<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="wrap_content">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textColor="@color/black"
        android:textSize="15sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0"
        tools:text="Item Number 1" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:contentDescription="@string/drop_down_arrow"
        android:src="@drawable/ic_arrow_drop_down"
        app:layout_constraintBottom_toBottomOf="@id/textView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@id/textView" />

    <LinearLayout
        android:id="@+id/expandableLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textView">

        <CheckBox
            android:id="@+id/checkbox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="@string/do_you_want_to_check_this_item" />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
```

The adapter is where the core logic exists. What we'll do here is store a list of all items that are expanded. 

```kotlin
private var expandedItems: ArrayList<ItemData> = arrayListOf()
```

And every time an item is clicked upon, we simply add/remove the item from this list and call `notifyItemChanged()` for that position. 

```kotlin
binding.textView.setOnClickListener {
    if (expandedItems.contains(item)) {
        expandedItems.remove(item)
    } else {
        expandedItems.add(item)
    }
    notifyItemChanged(bindingAdapterPosition)
}
```

By doing this, we achieve a neat little expanding and collapsing animation that is automatically handled by Android. The `notifyItemChanged()` is the secret formula.

> Notice we do not change any UI elements here. All we do is simply change the state. That's because our bind() method is called and it takes care of displaying the appropriate views based on the state. 

The ViewHolder class contains a `bind()` method that sets up the item view. 

```kotlin
fun bind(item: ItemData) {
    val isExpanded = expandedItems.contains(item)
    binding.textView.text = item.itemText
    binding.checkbox.isChecked = item.isChecked
    binding.imageView.setImageResource(
        if (isExpanded) R.drawable.ic_arrow_drop_up
        else R.drawable.ic_arrow_drop_down
    )
    binding.expandableLayout.isVisible = isExpanded

    binding.textView.setOnClickListener {
        if (expandedItems.contains(item)) {
            expandedItems.remove(item)
        } else {
            expandedItems.add(item)
        }
        notifyItemChanged(bindingAdapterPosition)
    }

    binding.checkbox.setOnClickListener {
        val isChecked = binding.checkbox.isChecked
        itemList[bindingAdapterPosition].isChecked = isChecked
        listener.onCheckChanged(bindingAdapterPosition, isChecked)
    }
}
```

`ItemData` is just a data class I created to store the state of each item in the RecyclerView. 

```kotlin
data class ItemData(
    var itemText: String,
    var isChecked: Boolean = false
)
```

The adapter accepts an Interface object which allows for communication.
```kotlin
interface OnItemCheckChangedListener {
    fun onCheckChanged(position: Int, isChecked: Boolean)
}
```

You can find the full source code in my [Repository](https://github.com/rdias002/expandable-recyclerview-demo)