DEV Community

Ralph Dias
Ralph Dias

Posted on

How to create an Expandable Item in Android RecyclerView?

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

We start with a simple RecyclerView in MainActivity.kt.

<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"/>
Enter fullscreen mode Exit fullscreen mode

The list_item_view.xml will contain the views to display when the item is expanded and collapsed.

<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>
Enter fullscreen mode Exit fullscreen mode

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

private var expandedItems: ArrayList<ItemData> = arrayListOf()
Enter fullscreen mode Exit fullscreen mode

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

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

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.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

data class ItemData(
    var itemText: String,
    var isChecked: Boolean = false
)
Enter fullscreen mode Exit fullscreen mode

The adapter accepts an Interface object which allows for communication.

interface OnItemCheckChangedListener {
    fun onCheckChanged(position: Int, isChecked: Boolean)
}
Enter fullscreen mode Exit fullscreen mode

You can find the full source code in my Repository

Oldest comments (0)