Nested recycler in Android done right!

Jakub Minarik
Nerd For Tech
Published in
5 min readFeb 22, 2021
Photo by Mika Baumeister on Unsplash

TL;DR: This article solves the problem of horizontal scroll position lost when scrolling vertically and horizontal scroll gestures being registered as vertical. Refer to this GitHub repo with sample app to see solution.

Many apps including Netflix and Play Store use the nested recycler pattern of having multiple horizontal scrollable views embedded inside a vertical one. Implementing such structure in Android with RecyclerView seemed quite straightforward. And it is!

BUT… there are a couple of things that need to be tweaked for everything to be smooth and behave as expected. Let’s jump straight into it.

Let’s create an app that shows sections with animals. Each section has a title and each animal has a name and an image. The result should look something like this:

Sample app screenshot: nested recyclers with sections of animals.

Data

The data structure is following:

Layouts

We need a layout for the individual animal. The layout item_animal.xml contains a CardView with an ImageView, a TextView and a gradient to make the text pop.

Next, we need the layout for the section with the title and the nested RecyclerView: item_animal_section.xml

The activity_main.xml contains just a recycler.

Adapters

Next, we will need our adapters. For the sake of this tutorial, I am using a simple RecyclerView.Adapter. In real life, you’d probably wanna go for RecyclerView.ListAdapter.

The AnimalAdapter.kt uses item_animal.xml and onBindViewHolder method sets the name and loads the image (using Coil library):

The AnimalSectionAdapter.kt uses item_animal_section.xml and onBindViewHolder method sets the title of the section and the adapter of the nested recycler:

Putting it all together

For the parent recycler (vertical), we’ll be using a ConcatAdapter. This is technically not necessary, but is a good habit as it will make your life easier if you decide to build on this. The structure is illustrated in the following image (MS Paint skills: 10/10):

Individual adapters illustrated.

All we need to do now is create some fake data (this is not really the point of this tutorial; check out my DataSource at the GitHub repo to see how I came around creating the lists, or create your own logic) and populate the adapters.

MainActivity.kt:

And voilà, we’ve created working nested RecyclerViews!

But have we really?

The issues

1. Recycling the recyclers

The first issue is caused by the fact that the individual views in our AnimalSectionAdapter (meaning the whole rows) are being recycled. This results in the horizontal scroll position being lost when scrolling vertically:

The horizontal scroll position of the top row is lost when scrolled vertically.

To resolve this, we need to manually save and restore horizontal scroll state of each row when it gets recycled or bound respectively.

To achieve this, we need to to save the state in the onViewRecycled method inside of our AnimalSectionAdapter and restostore the state in the onBindViewHolder method.

To persist the states, will use a MutableMap with the key being the ID of the corresponding row.

AnimalSectionAdapter.kt

2. Horizontal swipes registered as vertical

The second issue comes from the fact that we have two views with opposite scroll directions inside of each other. This has already been tackled by Christophe Beyls in his post (read it!) and instead of reinventing the wheel, we’ll use his brilliant solution.

Based on Christophe’s solution, we’ll create a Kotlin extension to solve this issue.

.

And call it in our MainActivity.kt’s initViews method:

Bonus: Optimize!

View pool

Each nested RecyclerView will by default have its own pool of views to recycle. This is not optimal, as we know the views are always identical so one pool would be enough for all of them.

We can easily achieve this by creating one pool inside of our AnimalSectionAdapter and setting it to each of the nested RecyclerViews.

Initial prefetch item count

As pointed out by Christophe Beyls himself:

On the nested RecyclerView’s LinearLayoutManager, you should call setInitialPrefetechItemCount() with the estimated number of horizontal items that will be visible. When prefetching the ViewHolder containing the horizontal RecyclerView, the parent RecyclerView will ask the child RecyclerView to pre-bind a full row of items and since the child RecyclerView can't know how many items will be displayed until it's laid out, you need to provide that information yourself. If you don't, only 2 items will be pre-bound by default, and the rest will be bound after prefetch when the row becomes laid out and visible which causes slower performance.

We will do exactly that inside the AnimalSectionAdapter’s onBindViewHolder:

Conclusion

Full code available at the this GitHub repo.

Building a fairly complex nested RecyclerView layout is such a common task that one would expect it to be working perfectly out-of-the box. In the Android world, however, this is unfortunately still not the case. I tackled the issue in this post, creating a sample app you can use to solve a similar issue of your own. Good luck!

--

--