Restoring scroll position of nested RecyclerViews

Cesar Morigaki
4 min readFeb 10, 2021
Photo by Ali Rizvi on Unsplash

Introduction

RecyclerView is a fundamental Android component used to build many screens. Its flexibility gives us the power to build complex UI as a vertical list containing carousels as items.

Netflix and Spotify

If you ever tried to create an interface similar to the above image, you probably faced losing the scroll state position of the nested RecyclerViews. Let’s understand why it happens and what is a possible solution.

Scenario

Let’s consider the following example:

Nested RecyclerViews

The straightforward implementation is: a RecyclerView using a vertical LinearLayoutManager and a ListAdapter handling 2 types, one of which is a View containing a RecyclerView using a horizontal LinearLayoutManager. This combination creates what we call nested RecyclerViews.

Problems

The example has two different state problems:

  1. When scrolling vertically, horizontal scroll state positions become a mess.
  2. When navigating forward and back, nested RecyclerViews lose their horizontal scroll position, scrolling to the initial position.
Vertical scroll (left) — Fragment view recreation (right)

Under the hood

First, we need to understand how the component works before proposing a solution. We have two different problems caused by two different behaviours.

First scenario: Scrolling vertically
Source of the problem: View recycling

RecyclerView has a view recycling mechanism to display content in a performative way. It has a pool of views and only a minimum quantity of instances is created to be reused. When we scroll up, the view that went out from RecyclerView bounds is recycled and added to the pool. For the view that will appear on the bottom, a view is requested from the pool or a new instance is created if none is available.

Therefore, our view containing the nested RecyclerView is being reused and the scroll position is at the same state it was when recycled. This happens because the state position is not something we set onBindViewHolder().

Second scenario: Navigation
Source of the problem: View restoration

When we leave the fragment, its view is destroyed to save device resources. On return, the view is recreated and the view state is restored.

All Android framework-provided views have their own implementation of onSaveInstanceState() and onRestoreInstanceState(), so you don't have to manage view state within your fragment.

But in our scenario, only the main RecyclerView has its scroll state restored. This happens because each view of the hierarchy starting from the root is responsible to call the function to store and restore state. RecyclerView implementation calls onSaveInstanceState()/onRestoreInstanceState() from only its LinearLayoutManager (that holds the scroll position state) and do not make the call to its nested components.

Solution

I'm presenting a possible implementation using ListAdapter inheritance but we can adapt it using composition.

For the first problem, we need to store the state for each view that is being recycled and restore itonBindViewHolder().

For the second problem, we also need to save the state from the visible views. We track them when onBindViewHolder() is called and save the state when a new submitList() is called. This implementation also works for content refreshing.

I'll not go through the implementation details. It should be clear reading the code comments and running the sample project. Also, this solution does not cover the Fragment recreation scenario but it's viable using the same solution key points.

Conclusion

RecyclerView is a complex component that hides many "secrets". By understanding how it works, it's possible to fill some gaps as we did for nested RecyclerViews.

--

--