ConcatAdapter Deep Dive
Learn about ConcatAdapter.
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
: Iffalse
, 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 optionfalse
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 totrue
to separate view types between adapters to avoid using the same view holder.stableIdMode
: There are threeStableIdMode
NO_STABLE_IDS
: Default value; ignores the stable id of the adapter added to theconcatAdapter
. 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 theNestedAdapterWrapper
.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 forgetItemViewType
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 returnRecyclerView.NO_ID
(default)SharedPoolStableIdStorage
: Return the adapter’sgetItemId
value as it isIsolatedStableIdStorage
: If thegetItemId
value of the adapter received as a parameter is already stored, return theid
or create a uniqueid
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
- index inspection: When we hand over the
adapter
andindex
to add them to theaddAdapter
, we first check whether thisindex
is a value between 0 and the current number of adapters. If you try to add anindex
value beyond this range,IndexOutOfBoundsException
occurs. If you call addAdapter without passing theindex
, you will add theadapter
internally in the last order. - Stable id check: Exception occurs when
StableIdMode
of the current ConcatAdapter is notNO_STABLE_IDS
andhasStableId
of theadapter
you are adding istrue
. IfStableIdMode
isNO_STABLE_IDS
and thehasStableIds
of theadapter
you want to add istrue
, it will log the warning. - Check whether the adapter has already been added: If the
adapter
already added to the ConcatAdapter matches theadapter
you want to add, return it without adding the adapter. - 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 theonAttachedToRecyclerView
method, and if the item already exists on the adapter you are adding, callnotifyItemRangeInserted
to update the list. - 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.
- If the
mReusableHolder
cached using themInUse
flag value of themReusableHolder
field held by the Controller is not used, create a newWrapperAndLocalPosition
object. - Use a recurring statement to subtract the cached
List<Wrapper>
field from theglobalPosition
by the number of items in the cached wrapper. - If the number of items in the wrapper is larger than the
globalPosition
, update thewrapper
andposition
of the createdwrapperAndLocalPosition
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. 🙂