ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

MergeAdapter/ConcatAdapter the first glance

Photo by Markus Winkler on Unsplash

The MergeAdapter has renamed to ConcatAdapter from1.2.0-alpha04. https://developer.android.com/jetpack/androidx/releases/recyclerview#recyclerview-1.2.0-alpha04

For Android developer RecyclerView probably is the most important UI class we should know and master because it’s useful and suitable for most of the use cases.

RecyclerView is powerful but still quite basic so lot’s of extended tools like DiffUtil, ListAdapter, SortedList, etc., are built to help us easily achieve the complex business logic for different scenario.

It never ends. If you want to support sectionalized data or add header/footer we still have to add some additional logic inside the Adapter. Here are some strategy I often use (and probably you’re familiar as well):

  1. Keep the original data and do adjustment when we use it in getItemCount, onBindViewHolder, etc.
  2. Modify the original data every time we receive the update.

Either way is not complex but easy to mess up and didn’t scale well, we need to change the logic inside the adapter every time we want to add more sections or header/footer. It’d be better if we have a high level solution instead of manipulate the offset by ourself.

That’s why MergeAdapter is here to the rescue.

MergeAdapter

As the name imply, MergeAdapter is a tool we can use to combine different Adapter together to separate logic and reuse in Adapter level. It’s so simple that you can easily know how to use it by just few lines of code here.

val adapterA: AdapterA = …
val adapterB: AdapterB = …
val mergeAdapter = MergeAdapter(adapterA, adapterB)
recyclerView.adapter = mergeAdapter
val adapterC: AdapterC = …
mergeAdapter.addAdapter(adapterC)

Yes, that’s it.

MergeAdapter will handle rest of the things for us and we can dynamically add or remove anAdapter or change the data in each Adapter without breaking anything.

If you’re not satisfying let’s keep digging more.

How it works

Let’s spend a few minutes to think about if we have bunch of Adapter we want to merge together like MergeAdapter do, how can we do it?

First, we need to make MergeAdapter extends fromAdapter as it’s still an Adapter from outside. Here is the minimal functions we need to create an Adapter.

class ImagineAdapter: RecyclerView.Adapter() {  override fun getItemCount(): Int  override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int): RecyclerView.ViewHolder
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int)
fun addAdapter(adapter: RecyclerView.Adapter) fun removeAdapter(adapter: RecyclerView.Adapter)}

Here are some missing part we need to fulfill:

  1. We need to know the total number of items in each Adapter.
  2. Each ViewHolder will be created by its corresponding ViewType.
  3. We’ll need to bind the correct pair of data and ViewHolder in onBindViewHolder.

Wrapper

The pattern we use here is called Wrapper — or if you preferred Decorator pattern is often used to solve problem like ours, we create a decorator class implement the original interface(Adapter) with new functionality (Add/Remove child Adapter) and wrap the original class(actually a list in our case) and then we can use the decorator class directly without changing any of the existing interfaces.

The slight difference is we wrap a bunch of Adapter instead so we can’t just bypass the parameter/result we have, we need to calculate the correct value internally like something below:

// ImagineAdapterval list: List<RecyclerView.Adapter<RecyclerView.ViewHolder>>

override fun getItemCount(): Int {
return list.sumBy { it.itemCount }
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
var index = 0
var localPosition = position
while (localPosition >= list[index].itemCount) {
localPosition -= list[index++].itemCount
}
list[index].onBindViewHolder(holder, localPosition)
}

The getItemCount is easy since we can sum all the result from child getItemCount, but we need things like onBindViewHolder to calculate the correct offset first before pass it to the child Adapter. For example we have three adapters inside MergeAdapter and each has 10 items, the getItemCount will be 30, and if we get a holder and position is 27 in onBindViewHolder call, we need to call the 3rd adapter with the same holder and position 7 instead.

This is indeed something we need to be careful about in every functions. But it’ll become stable once we finish, as the pattern is quite the same and no need to change. You can extract the same calculation logic to another function and reuse it every time as well.

ViewType

Even though the calculation is tedious along with small overhead, it’s not a big issue for now. The actual first problem we’ll have is that we don’t know the relationship of the ViewType in each Adapter and the MergeAdapter. Every child Adapter may have the same ViewType but relate to different ViewHolder because each Adapter shouldn’t know if other Adapter is already use the same ViewType or not.

It’s an interesting situation and we can have several options for this. If you build this only for yourself, you can add some restrictions on ViewType to make things easier. One of the solutions might be to force each ViewType unique so you can use it directly, or ViewType should bound by some static number so you can allocate distinct partitions for each Adapter easily.

The solution Google provides is also quite open, we can determine if we have the global unique ViewType or we need to isolate the ViewType in each Adapter by setting Config at the MergeAdapter constructor.

val config = MergeAdapter.Config.Builder()
.setIsolateViewTypes(false)
.build()
val adapter = MergeAdapter(config)

By setting isolateViewTypes to false we indicate we’ll have the global unique ViewType which also gain a bit of performance as we earlier mentioned there’s no mapping needed. Otherwise we need an additional map to keep track of ViewType mapping from adapter to adapter in whatever logic.

p.s. You can find ViewTypeStorage to see how MergeAdapter deal with the ViewType issue we mentioned above.

Notify

The second problem we have is how can we notify RecyclerView to update if the data is changed.

In order to solve this we need to understand how the notify the mechanism work first. Let’s go to RecyclerView class to find out the related code.

//RecyclerViewprivate final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();public void setAdapter(@Nullable Adapter adapter) {
setAdapterInternal(adapter, false, true);
}
private void setAdapterInternal(@Nullable Adapter adapter,
boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
}

mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
}
}

We found RecyclerView has a RecyclerViewDataObserver which will pass to Adapter to register events when we called setAdapter. And RecyclerViewDataObserver is a subclass of AdapterDataObserver which looks like what we need.

public abstract static class AdapterDataObserver {
public void onChanged()

public void onItemRangeChanged(int positionStart, int itemCount)

public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload)

public void onItemRangeInserted(int positionStart, int itemCount)

public void onItemRangeRemoved(int positionStart, int itemCount)

public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount)

public void onStateRestorationPolicyChanged()
}

Let’s go back to Adapter to see how observe works.

// RecyclerView.Adapterpublic void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
public final void notifyItemChanged(int position) {
mObservable.notifyItemRangeChanged(position, 1);
}
public final void notifyItemInserted(int position) {
mObservable.notifyItemRangeInserted(position, 1);
}
public final void notifyItemMoved(int fromPosition, int toPosition) {
mObservable.notifyItemMoved(fromPosition, toPosition);
}
public final void notifyItemRemoved(int position) {
mObservable.notifyItemRangeRemoved(position, 1);
}

In registerAdapterDataObserver we attach the observer to mObservable and when we call any notify function like notifyItemRemoved, mObservable will be triggered and then observer will be notified about the change we made.

So the original notify function in each child Adapter will not work since there’s no setAdapter occurred when we have a MergeAdapter on top, but we can link them gracefully by calling registerAdapterDataObserver in addAdapter to bind the observer from childAdapter to MergeAdapter. Just remember before we pop the event to RecyclerView, we need to adjust the offset like we always do.

p.s. Check NestedAdapterWrapper.Callback on how MergeAdapter link the notify chain we talked here.

There are things we didn’t covered like setHasStableIds or other properties in this article, but overall you may apply the some strategy again and again. The sample code may differ from the actual source code in MergeAdapter but the concept will remain the same and it’s also my intention to not use the actual source code to highlight the thing we talk. Another reason is that it’s still in alpha so every thing might change but most important thing is that I wish you can have your own joy when reading the source.

Thanks for reading, and if you like this article please give me claps or share. Also don’t hesitate to leave comments if you have any feedback.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Jintin

Android/iOS developer, husband and dad. Love to build interesting things to make life easier.

Responses (1)

Write a response

Hi Jintin,
Great article can you share source code for this

--