Concatenate adapters sequentially with ConcatAdapter
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.
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
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.
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:
LoadState, exposed from the
- A load state header and footer layout
ViewHolderobject for the header and footer
ListAdapterthat displays 0 or 1 items based on the
LoadState. Every time the
LoadStatechanges, we notify that the item needs to be changed, inserted or removed (see code).
🔎 More about ConcatAdapter
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:
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
notifyItemRangeChanged means items are the same, just their contents changed.
notifyDataSetChanged means there is no relation between before and after. Hence, we cannot map
If an adapter calls
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
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
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