Maximizing Efficiency: Using a Single Adapter for Different View Holders in RecyclerView

Anna Karenina Jusuf
6 min readAug 19, 2023

--

Hi, it’s been ages since the last time I write something here.

As an android developer, we are often dealing with Recycler View that needs an Adapter which chained with View Holder to Bind the data.. like always. Those are components that can’t be separated when it comes to implementing a list of data. In this article I want to share a little tips on how we can optimize the code by using only one Adapter to achieve so.

First thing first

Create the view with ConstraintLayout and RecyclerView in it. Here I take one from my simple project called ‘Learn Crypto

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dimen_16"
android:layout_marginEnd="@dimen/dimen_16"
android:layout_marginTop="@dimen/dimen_16"
tools:context=".features.coins.CoinsFragment">

<TextView
android:id="@+id/textViewCoins"
style="@style/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/coins"
android:textColor="@color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recylerViewCoins"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/dimen_8"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewCoins"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/item_coins_layout"/>

<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:visibility="gone"
android:layout_height="wrap_content"
android:indeterminateTint="@color/primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

and the item layout

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_coins"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen_8"
android:backgroundTint="@color/primaryAccent"
app:cardCornerRadius="15dp">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">

<TextView
android:id="@+id/coin_name"
style="@style/title"
android:layout_width="wrap_content"
android:maxEms="5"
android:maxLines="2"
android:layout_height="wrap_content"
android:textColor="@color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Bitcoin" />

<include
android:id="@+id/status_active"
layout="@layout/item_indicator_status_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_4"
android:layout_marginStart="@dimen/dimen_8"
app:layout_constraintStart_toEndOf="@+id/coin_name"
app:layout_constraintTop_toTopOf="@+id/coin_name" />

<include
android:id="@+id/status_new"
layout="@layout/item_indicator_status_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_4"
android:layout_marginStart="@dimen/dimen_4"
app:layout_constraintStart_toEndOf="@+id/status_active"
app:layout_constraintTop_toTopOf="@+id/coin_name" />

<TextView
android:id="@+id/coin_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_4"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/coin_name"
tools:text="BTC" />

<TextView
android:id="@+id/label_coin_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_4"
android:text="@string/type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/coin_symbol" />


<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rank"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/coin_rank"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintEnd_toEndOf="@+id/textView7"
app:layout_constraintStart_toStartOf="@+id/textView7"
app:layout_constraintTop_toBottomOf="@+id/textView7"
tools:text="1" />
</androidx.constraintlayout.widget.ConstraintLayout>


</com.google.android.material.card.MaterialCardView>

next the most important, create the BaseAdapter.kt

class BaseAdapter<Model : Any, ViewHolder : RecyclerView.ViewHolder>
(
private val onCreateViewHolder : (parent : ViewGroup, viewType : Int) -> ViewHolder,
private val onBindViewHolder : (viewHolder : ViewHolder, position : Int, item : Model) -> Unit,
private val differCallback : DiffUtil.ItemCallback<Model>,
private val onViewType : ((viewType : Int, item : List<Model>) -> Int)? = null,
private val onDetachFromWindow : ((ViewHolder) -> Unit)? = null
) : RecyclerView.Adapter<ViewHolder>()
{
var item = listOf<Model>()
private var onGetItemViewType: ((position : Int) -> Int)? = null
val differ = AsyncListDiffer(this, differCallback)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = onCreateViewHolder.invoke(parent,viewType)

override fun onBindViewHolder(holder: ViewHolder, position: Int)
{
val item = differ.currentList[position]
onBindViewHolder.invoke(holder,position, item)
}

override fun getItemCount(): Int = differ.currentList.size

override fun getItemViewType(position: Int): Int
{
return if (onViewType != null)
{
onViewType.invoke(position, item)
} else
{
val onGetItemViewType = onGetItemViewType
onGetItemViewType?.invoke(position) ?: super.getItemViewType(position)
}
}

override fun onViewDetachedFromWindow(holder: ViewHolder)
{
super.onViewDetachedFromWindow(holder)
onDetachFromWindow?.invoke(holder)
}
}

i will explain the BaseAdapter.kt sequentially,

the BaseAdapter.kt class have 2 generic parameter <Model : Any, ViewHolder : RecyclerView.ViewHolder> specifies the type parameters for the class. Model is a type parameter that represents the data model, and ViewHolder is a type parameter that represents the ViewHolder type.

5 constructor parameter with a Higher Order Function as the parameters type except for the differCallback it takes DiffUtil.ItemCallback as the parameter type, the first 3 of them is non null parameter.

onCreateViewHolder:
A lambda that takes a parent ViewGroup and a viewType Int as parameters, and returns a ViewHolder. It’s responsible for inflating the layout and creating a new ViewHolder instance.

onBindViewHolder:
A lambda that takes a viewHolder, position, and an item of type Model as parameters. It’s responsible for binding data to the ViewHolder at a given position.

differCallback:
An instance of DiffUtil.ItemCallback<Model> used for calculating the difference between old and new data items. It’s used to efficiently update the RecyclerView’s items.

we are using DiffUtil here to optimizing the performance of RecylerView, read more about DiffUtil in the amazing article by kak Vero.

onViewType (optional):
A lambda that takes a viewType and a list of Model items, and returns a viewType as an Int. This can be used to define different view types within the adapter.

onDetachFromWindow (optional):
A lambda that takes a ViewHolder as a parameter. It’s called when a ViewHolder is detached from the window, which can be useful for performing cleanup.

The class extends RecyclerView.Adapter<ViewHolder>(), which means it’s inheriting from the RecyclerView.Adapter class and is specialized to work with the provided ViewHolder type.

enough for the parameters, lets see what the class do. There are some variable declaration :
item is a private property that holds the list of data items that will be displayed by the adapter.
onGetItemViewType is a lambda that determines the view type for a given position. It takes a position and returns a view type integer. This lambda is set to null initially and can be customized externally.
differ is an instance of AsyncListDiffer, a utility class that calculates differences between old and new lists efficiently. It uses a differCallback to detect changes.

and some Overridden function which are :
onCreateViewHolder:
This function is overridden to create a ViewHolder using the onCreateViewHolder lambda that was provided when constructing the adapter. It uses the invoke function of the lambda to create the ViewHolder.
onBindViewHolder:
This function is overridden to bind data to a ViewHolder using the onBindViewHolder lambda provided when constructing the adapter. It retrieves the current item from the AsyncListDiffer and invokes the lambda with the ViewHolder and item.
getItemCount:
do i really have to explain this one? lol jk, as the name itself it is use to return the size of the current list managed by the AsyncListDiffer.
getItemViewType:
This function is overridden to determine the view type of a particular item at a given position. It first checks if the onViewType lambda is provided. If it is, it invokes the lambda with the position and the item list. If not, it either uses the custom onGetItemViewType lambda if provided, or falls back to the default behavior.
last but not least, onViewDetachedFromWindow:
This function is overridden to handle the detachment of a ViewHolder from the window. It invokes the onDetachFromWindow lambda if provided.

move to the next step, let’s create the ViewHolder Class, this class is basically to inflate the item layout into the view holder, declare the diff util callback, data binding from model to item layout and defining the click action.

class CoinViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView)
{
companion object
{
fun inflate(parent : ViewGroup) : CoinViewHolder
{
return CoinViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_coins_layout,parent,false)
)
}

val differCallback = object : DiffUtil.ItemCallback<CoinsResponseItem>(){
override fun areItemsTheSame(oldItem: CoinsResponseItem, newItem: CoinsResponseItem): Boolean
{
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: CoinsResponseItem, newItem: CoinsResponseItem): Boolean
{
return oldItem == newItem
}
}
}

fun bind(coinsResponseItem: CoinsResponseItem)
{
with(itemView)
{
coinsResponseItem.apply()
{
findViewById<TextView>(R.id.coin_name).text = name
findViewById<TextView>(R.id.coin_rank).text = rank.toString()
findViewById<TextView>(R.id.coin_symbol).text = symbol
findViewById<TextView>(R.id.label_coin_type).text = resources.getString(R.string.type, type)
findViewById<CardView>(R.id.status_active).findViewById<TextView>(R.id.textViewStatus).text = resources.getString(UtilitiesFunction.convertBooleanToActiveOrNotActive(isActive))
if (isNew)
{
findViewById<CardView>(R.id.status_new).findViewById<TextView>(R.id.textViewStatus).text = resources.getString(UtilitiesFunction.convertBooleanToNew(isNew))
findViewById<CardView>(R.id.status_new).setCardBackgroundColor(resources.getColor(R.color.teal_700))
}
else
{
findViewById<CardView>(R.id.status_new).findViewById<TextView>(R.id.textViewStatus).visibility = View.GONE
}
}
}
}

fun setAction(action : (position : Int) -> Unit)
{
itemView.setOnClickListener()
{
action.invoke(adapterPosition)
}

}
}

Now let’s see how we can use the Adapter over and over again with different types of Model and ViewHolder.

In the CoinsFragment Class, call the BaseAdapter, define the Model as well as the ViewHolder and setup the adapter just like this code below.

class CoinsFragment : BaseFragment<CoinsViewModel>() 
{
private var _bindingCoinsFragment : FragmentCoinsBinding? = null
private val _getbindingCoinsFragment get() = _bindingCoinsFragment

private lateinit var _coinAdapter : BaseAdapter<CoinsResponseItem, CoinViewHolder>
private val _lazyCoinAdapter by lazy { _coinAdapter }

override fun onViewCreated(view: View, savedInstanceState: Bundle?)
{
super.onViewCreated(view, savedInstanceState)
setupAdapter()
displayView()
observeLiveData()
}

private fun setupAdapter()
{
_coinAdapter = BaseAdapter(
{ parent, _ -> CoinViewHolder.inflate(parent) },
{ viewHolder, _, item -> viewHolder.bind(item)
viewHolder.setAction {
val bundle = Bundle()
bundle.putString(ID_COIN_CONSTANT,item.id)
UtilitiesFunction.replaceFragment(parentFragmentManager, CoinDetailFragment(),bundle)
}
},
CoinViewHolder.differCallback
)
}

private fun observeLiveData()
{
_viewModel.coins.observe(viewLifecycleOwner)
{ response ->
when(response)
{
is ApiResponse.Success ->
{
_activityMain._dialog.hide()
response.data?.let { _lazyCoinAdapter.differ.submitList(it) }
}
}
}
}

private fun displayView()
{
_bindingCoinsFragment?.recylerViewCoins.apply {
this?.setVertical()
this?.adapter = _lazyCoinAdapter
}
}
}

That’s really it, the next time you have another Recycler View with a different Item Layout, all you have to do is just create the View Holder and then call the BaseAdapter and define the Model as well as the ViewHolder, no more creating the Adapter.

Thank you for reading, if there is any misconception/misunderstanding or anything i can improve please let me know so i can do better :)

--

--