ConcatAdapter Deep Dive

hongbeom
ProAndroidDev
Published in
6 min readFeb 25, 2023

--

Learn about ConcatAdapter.

Photo by Homemade Media on Unsplash

ConcatAdapter

ConcatAdapter is an adapter that allows you to add multiple adapters that appeared in the version RecyclerView : 1.2.0-alpha02. In the past, RecyclerView.Adapter had to provides view type to each by view in order to show each view in a list form. This led to the presence of multiple ViewHolder on one adapter, and multiple logics were combined. ConcatAdapter is implemented to add multiple adapters to a single adapter and to add them to a single RecyclerView to benefit from encapsulation and reuse.

이미지

Component in ConcatAdapter

Config

You can specify default options by passing the ConcatAdapter.Config object to the constructor of the ConcatAdapter.

You can control the isolateViewTypes and stableIdMode in the Config, each of which is described below.

  • isolateViewTypes : If false, the ConcatAdapter assumes that all adapters assigned to it share a global view type pool so that they reference the same view holder using the same view type. Specifying this option false means that nested adapters can share view holders, but they should not have conflicting view types that return the same view type for different view holders. Defaults to true to separate view types between adapters to avoid using the same view holder.
  • stableIdMode : There are three StableIdMode
  1. NO_STABLE_IDS : Default value; ignores the stable id of the adapter added to the concatAdapter. If you add an adapter that uses stable id when in this mode, a warning is issued.

2. ISOLATED_STABLE_IDS : In this mode, hasStableIds() of the concatAdapter returns true and requires an adapter with a stable id specified. Because two different adapters may not recognize each other and return the same stable id, ConcatAdapter separates the id pool of each adapter from each other, overwrites the returned stable id, and notifies RecyclerView again. In this mode, the value of RecyclerView.Adapter.getItemId(int) and the value of RecyclerView.ViewHolder.getItemId() may differ. Additionally, if you add an adapter with no stable id specified, an IlllegalArgumentException error occurs.

3. SHARED_STABLE_IDS : Similar to ISOLATED_STABLE_IDS, hasStableIds() returns true and requires an adapter with a stable id. However, unlike ISOLATED_STABLE_IDS, it does not override the returned stable id. In this mode, the sub-adapters must be aware of each other and should not return the same id unless items are moved between the adapters. IllegalArgumentException error occurs when adding an adapter with no stable id specified.

⚠️ If you create a Constructor in ConcatAdapter without handing over the Config, it is designated as Config.DEFAULT, isolateViewTypes as true, and StableIdMode as NO_STABLE_IDS

NestedAdapterWrapper

Most functions in ConcatAdapter delegate actions to the ConcatAdapterController. And the ConcatAdapterController implements NestedAdapterWrapper.Callback, so first let’s look at NestedAdapterWrapper.

NestedAdapterWrapper is a class that wraps the adapter we added. Callback looks the same as RecyclerView.AdapterDataObserver, but when we add the Adapter through the addAdapter method, the Adapter and ConcatAdapterController that we want to add to the constructor’s parameter in NestedAdapterWrapper are handed over, and NestedAdapterWrapper takes over Callback (ConcatAdapterController) is called by mapping to each method in the AdapterDataObserver that it has as an internal field.

The code above shows that NestedAdapterWrapper has an object called ViewTypeStorage.ViewTypeLookup, StableIdStorage.StableIdLookup as a field. These fields determine the value returned when we look up the id or view type of a particular item in ConcatAdapter.

ViewTypeStorage

ViewTypeStorage is a storage interface that stores and manages view types. It is held as a field by the ConcatAdapterController.

getItemViewType

When we call the getItemViewType function, the viewtype is eventually returned by NestedAdapterWrapper’s getItemViewType, which is determined by the ViewTypeStorage.ViewTypeLookup interface implementation held in the field.

In addition, ViewTypeStorage is created from the constructor of the ConcatAdapterController, which is divided into two implementations, depending on the value of the isolateViewTypes.

IsolatedViewTypeStorage, SharedIdRangeViewTypeStorage, is declared inside the WrapperViewTypeLookup, the implementation class of the ViewTypeStorage.ViewTypeLookup interface, which returns the view type by referring to the view type managed by the corresponding Storage.

  • SharedIdRangeViewTypeStorage: getItemViewType returns the view type of the adapter connected to the NestedAdapterWrapper.
  • IsolatedViewTypeStorage: getItemViewType returns if it is already stored, otherwise create a unique viewtype and return it. (Even if N adapters with the same view type are added, different unique view types will be returned for getItemViewType

StableIdStorage

StableIdStorage is a storage interface that stores Stableid. Similarly, the ConcatAdapterController holds it as a field.

getItemId

Similarly, if the NestedAdapterWrapper holds StableIdLookup as a field and calls getItemId, this field returns id.

Implementations of StableIdLookup are declared inside three implementations of the StableIdStorage interface, each of which behaves in this way.

  • NoStableIdStorage: unconditionally return RecyclerView.NO_ID (default)
  • SharedPoolStableIdStorage: Return the adapter’s getItemId value as it is
  • IsolatedStableIdStorage: If the getItemId value of the adapter received as a parameter is already stored, return the id or create a unique id to return it.

Under the hood

addAdapter

What will happen internally if we call addAdapter to add one adapter? The ConcatAdapter calls the addAdapter of the ConcatAdapterController and the ConcatAdapterController follows the process below to perform the addAdapter.

Controller — addAdapter

  1. index inspection: When we hand over the adapter and index to add them to the addAdapter, we first check whether this index is a value between 0 and the current number of adapters. If you try to add an index value beyond this range, IndexOutOfBoundsException occurs. If you call addAdapter without passing the index, you will add the adapter internally in the last order.
  2. Stable id check: Exception occurs when StableIdMode of the current ConcatAdapter is not NO_STABLE_IDS and hasStableId of the adapter you are adding is true. If StableIdMode is NO_STABLE_IDS and the hasStableIds of the adapter you want to add is true, it will log the warning.
  3. Check whether the adapter has already been added: If the adapter already added to the ConcatAdapter matches the adapter you want to add, return it without adding the adapter.
  4. Once you have completed this process, create a NestedAdapterWrapper, connect the adapter you want to add, and cache it. In addition, notify the RecyclerView connected to the ConcatAdapter through the onAttachedToRecyclerView method, and if the item already exists on the adapter you are adding, call notifyItemRangeInserted to update the list.
  5. Finally, update the StateRestorationPolicy.

getItemViewType & getItemId

We can call ConcatAdapter’s getItemViewType to find out the view type of a particular location item. The ConcatAdapter then internally calls the ConcatAdapterController’s getItemViewType.

You can also call getItemId to find out the id of a particular location item. Similarly, call getItemId of ConcatAdapterController.

We’ve looked at how getItemViewType and getItemId call from wrapper get values from above, so let’s look at findWrapperAndLocalPosition and releaseWrapperAndLocalPosition.

findWrapperAndLocalPosition

findWrapperAndLocalPosition is a function that calculates the exact position using the globalPosition received as a parameter.

This uses the WrapperAndLocalPosition class, which is defined inside the ConcatAdapterController.

The findWrapperAndLocalPosition method returns a WrapperAndLocalPosition object with the correct position and corresponding Wrapper in the following procedure.

  1. If the mReusableHolder cached using the mInUse flag value of the mReusableHolder field held by the Controller is not used, create a new WrapperAndLocalPosition object.
  2. Use a recurring statement to subtract the cached List<Wrapper> field from the globalPosition by the number of items in the cached wrapper.
  3. If the number of items in the wrapper is larger than the globalPosition, update the wrapper and position of the created wrapperAndLocalPosition object and return it.

releaseWrapperAndLocalPosition

If mReusableHolder was used due to an action such as getItemId or getItemViewType, the releaseWrapperAndLocalPosition function is called at the end. This is to allow the wrapper and position to be returned without new assignments if possible.

Conclusion

Using ConcatAdapter, which represents different types of data as a single adapter and can encapsulate logic, may be a good idea. However, if you don’t understand and use the Config and internal behavior in detail, you may encounter unintended behavior. 🙂

Reference

--

--