Create an expandable RecyclerView with the ConcatAdapter

Houssein Ouerghemmi
Jun 16 · 5 min read

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.

Image for post
Image for post
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.

Image for post
Image for post
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 ?

override fun getItemCount(): Int {
return if (isExpanded)
itemsGroup.items.size + 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

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()
val concatAdapter = ConcatAdapter(concatAdapterConfig, adapters)

Animate the changes

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

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

Custom arrow animation

Image for post
Image for post
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.
Image for post
Image for post
  • 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.
Image for post
Image for post
  • 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.
Image for post
Image for post

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

Image for post
Image for post

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!


Learnings and insights from SFEIR community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store