Create an expandable RecyclerView with the ConcatAdapter

Houssein Ouerghemmi
CodeShake
Published in
5 min readJun 16, 2020

RecyclerView is among the most important Views in the Android framework. Since it’s released in 2014, it brings a lot of advantages compared to ListView. However, it has some limitations, it’s not simple to build complexes screens with it like a list of expandable nested lists.

Fortunately for us, the AndroidX team released a native solution for this with a new version of RecyclerView 1.2.0-alpha03 which support a new type of adapter: ConcatAdapter which enables us to sequentially combine multiple adapters to be displayed in a single RecyclerView.

One use case for the ConcatAdapter is doing an expandable list. There’s no simple solution to do this with the traditional RecyclerView, except using a third-party library, but with the ConcatAdapter it becomes easy to do.

Expandable RecycleView with the ConcatAdapter

My idea for this is to do one adapter per section, which has only the header and its nested list and concat all the adapters together.

One adapter per section

So first, let’s focus on how to create the section adapter, which contains a header and a nested list.
To simplify the data, I created a data class which contains the header and its items:

data class ItemsGroup(val title: String, val items: List<String>)

Our adapter has two view types: header and item.

How to expand/collapse the items ?

all the magic happens on the getItemCount method :

override fun getItemCount(): Int {
return if (isExpanded)
itemsGroup.items.size + 1
else
1
}

If the section is expanded, we return the section item count (the items and the header), otherwise 1 to display just the header. Then to expand or collapse the section we have to set to true or to false the isExpanded and we have to make sure that we call the notifyDatasetChanged to refresh the section.

Merging all the sections

Once we created the “adapters” (1 adapter per section), all we have to do is create the ConcatAdapter.

val concatAdapter = ConcatAdapter(adapters)
myRecyclerView.adapter = concatAdapter

By default, each adapter maintains its own pool of ViewHolder, with no re-use in between adapters. But in our case, all the adapters have the same ViewHolders (the Header ViewHolder and the Item ViewHolder), so for re-using them, we have to configure our adapter with setIsolateViewTypes :

val concatAdapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(false)
.build()
val concatAdapter = ConcatAdapter(concatAdapterConfig, adapters)

Animate the changes

RecyclerView brings a DefaultItemAnimator which enables us to update the list with some default animations, and in our case, it allows us to expand and collapse the items with an animation. The expand action is handled like items insertion and the collapse action is handled like items deletion.

So when we expand our section, we have to call notifyItemRangeInserted to tell the adapter that we have updated the list from position 1 with the items count. And if we have an icon in the header that shows the expand status, we have to update it too with notifyItemChanged(0)

notifyItemRangeInserted(1, itemsGroup.items.size)
//To update the header expand icon
notifyItemChanged(0)

and to collapse the list, we have to call the notifyItemRangeRemoved to notify the items remove and update the header status.

notifyItemRangeRemoved(1, itemsGroup.items.size)
//To update the header expand icon
notifyItemChanged(0)

Custom arrow animation

The arrow toggle animation

The DefaultItemAnimator can handle the expand/collapse animation with item insertion/deletion animations, but for the arrow in the header it’s handled by the default onChange animation which it’s the fade animation. So to create an arrow rotation (from 0 to 180 degrees), we have to create our custom ItemAnimator by extending the DefaultItemAnimator.

Our CustomItemAnimator is shared between the HeaderViewHolder and the ItemViewHolder, so I apply the customization only for the first one and keep the default behavior for the second.

The ItemAnimator has 3 interesting methods : recordPreLayoutInformation, recordPostLayoutInformation,animateChange.

  • recordPreLayoutInformation : called before the ViewHolder is changed, so here we have to record the necessary information about the animation, and in our case, it will be the place to get the start arrow rotation value.
https://github.com/OHoussein/Android-Expandable-ConcatAdapter/blob/master/app/src/main/java/dev/ohoussein/mergeadaptertutorial/ExpandableItemAnimator.kt#L12
  • recordPostLayoutInformation : called after the ViewHolder is changed, so here we record in record necessary information about the View’s final state, and in our case, it will be the place to get the target arrow rotation value.
https://github.com/OHoussein/Android-Expandable-MergeAdapter/blob/master/app/src/main/java/dev/ohoussein/mergeadaptertutorial/ExpandableItemAnimator.kt#L27
  • animateChange : Here we run our information between the states recorded in the two above methods. and once the animation is ended, we have to call dispatchAnimationFinished to tell that the ViewHolder is ready to use. In addition to the preLayoutInformationHolder and the postLayoutInformationHolder, this method takes in parameters the oldViewHolder and the newViewHolder which are the same when the method canReuseUpdatedViewHolder returns true, which it’s the default behavior.
https://github.com/OHoussein/Android-Expandable-ConcatAdapter/blob/master/app/src/main/java/dev/ohoussein/mergeadaptertutorial/ExpandableItemAnimator.kt#L40

And for the ItemInfo who holds the sate info between the old and the new holder:

https://github.com/OHoussein/Android-Expandable-ConcatAdapter/blob/master/app/src/main/java/dev/ohoussein/mergeadaptertutorial/ExpandableItemAnimator.kt#L68

Full source code is available here

There is a lot of other use cases of the ConcatAdapter, for instance we can concat adapters that come from multiple data sources. It makes the code simpler and the reusability of the Adapter.

Thank you for reading!

--

--