Illustration by Virginia Poltrack

Concatenate adapters sequentially with ConcatAdapter

Use case example: displaying a list header and footer

Florina Muntenescu
Apr 2, 2020 · 4 min read

ConcatAdapter is a new class available in recyclerview:1.2.0-alpha02 which enables you to sequentially combine multiple adapters to be displayed in a single RecyclerView. This enables you to better encapsulate your adapters rather than having to combine many data sources into a single adapter, keeping them focused and re-usable.

One use case for this is displaying a list loading state in a header or footer: when the list is retrieving data from the network, we want to show a progress spinner; in case of error, we want to show the error and a retry button.

A RecyclerView with a footer displaying the loading state: progress or error

Introducing ConcatAdapter

ConcatAdapter allows us to display the contents of multiple adapters, in a sequence. For example, let’s say that we have the following 3 adapters:

val firstAdapter: FirstAdapter = …
val secondAdapter: SecondAdapter = …
val thirdAdapter: ThirdAdapter = …
val concatAdapter = ConcatAdapter(firstAdapter, secondAdapter,
thirdAdapter)
recyclerView.adapter = concatAdapter

The recyclerView will display the items from each adapter sequentially.

Having different adapters allows you to better separate the concerns of each sequential part of a list. For example, if you want to display a header, you don’t need to put the logic related to the header display in the same adapter that handles the list display, rather you can encapsulate it in its own adapter.

RecyclerView and Adapter data

Displaying load state in a header and footer

Our header/footer displays either a progress indicator or reports an error. When the list has successfully finished loading, the header/footer shouldn’t display anything. Therefore they can be represented as a list with 0 or 1 items, with their own adapter:

val concatAdapter = ConcatAdapter(headerAdapter, listAdapter, 
footerAdapter)
recyclerView.adapter = concatAdapter

If both the header and the footer use the same layout, ViewHolder and UI logic (e.g when progress is displayed and how), you can implement just one Adapter class and create 2 instances of it: one for the header and one for the footer.

For a complete implementation, check out this pull request, which adds:

  • A LoadState, exposed from the ViewModel
  • A load state header and footer layout
  • A ViewHolder object for the header and footer
  • A ListAdapter that displays 0 or 1 items based on the LoadState. Every time the LoadState changes, we notify that the item needs to be changed, inserted or removed (see code).

🔎 More about ConcatAdapter

ViewHolders

By default, each adapter maintains their own pool of ViewHolders, with no re-use in between adapters. If multiple adapters display the same ViewHolder, we may want to reuse instances between them. We can achieve this by constructing our ConcatAdapter with a ConcatAdapter.Config object, where isolateViewTypes = false. Like this, all the adapters merged will use the same view pool. In the loading status header and footer example, both ViewHolders will actually display the same content so we could reuse them.

⚠️ To support different ViewHolder types, you should implement Adapter.getItemViewType. When you’re reusing ViewHolders, make sure that the same view type doesn’t point to different ViewHolders! One best practice for this is to return the layout ID as the view type.

Using stable ids

Instead of using stable ids along with notifyDataSetChanged, it is recommended to use the specific notify events of the adapter that give the RecyclerView more information about the changes in the data set. This allows the RecyclerView to update the UI more efficiently and with better animations. If you’re using ListAdapter then the notify events are handled for you, under the hood, with the help of the DiffUtil callback. But if you do need to use stable ids, the ConcatAdapter.Config provides 3 different configurations for stable ids: NO_STABLE_IDS, ISOLATED_STABLE_IDS and SHARED_STABLE_IDS. The last two require you to handle stable ids in your adapter. Check out the StableIdMode documentation for more information on how they work.

Data changes notifications

When an adapter part of a ConcatAdapter calls one of the notify functions, the ConcatAdapter computes the new item positions before updating the RecyclerView.

From the RecyclerView’s perspective, notifyItemRangeChanged means items are the same, just their contents changed. notifyDataSetChanged means there is no relation between before and after. Hence, we cannot map notifyDataSetChanged into notifyItemRangeChanged.

If an adapter calls Adapter.notifyDataSetChanged, ConcatAdapter will also call Adapter.notifyDataSetChanged, rather than Adapter.notifyItemRangeChanged. As usual with RecyclerViews avoid calling Adapter.notifyDataSetChanged(), prefer more granular updates or use an Adapter implementation that does this automatically, like ListAdapter or SortedList.

Finding ViewHolder position

You might have used ViewHolder.getAdapterPosition in the past to get the position of a ViewHolder in the adapter. Now, because we’re merging multiple adapters, use ViewHolder.getBindingAdapterPosition(). If you want to get the adapter that last bound a ViewHolder, in the case where you’re sharing ViewHolders, use ViewHolder.getBindingAdapter().

That’s all! If you want to sequentially show different types of data that would benefit from being encapsulated in their own adapters, start using ConcatAdapter. For advanced control of ViewHolder pool and stable ids, use ConcatAdapter.Config.

Android Developers

The official Android Developers publication on Medium