Stavros Georgiou
4 min readNov 26, 2020
Photo by Markus Spiske on Unsplash

Update: 29/11/2020

I have replaced the Kotlin reflection with just a simple mapping between view holders and enums. Thank you for your feedback!

The main problem of the RecyclerView Adapter is the boiler code that you need to write on every adapter. This can be huge if the adapter is going to handle different types of ViewHolders. I am going to show you how to build one adapter that can be used for every recycler view with minimal/clean code.

RecyclerViewItemType

First we need to build a helper enum that can be used for mapping the ViewHolders with the Data Classes (UI Data).

enum class RecyclerViewItemType(var viewType: Int) {
CARD_VIEWS(1),
DETAILS_VIEW(2),
BUTTON_VIEW(3),
HEADER_VIEW(4);
companion object { private val map = values().associateBy { it.viewType } private fun mapViewType(viewType: Int) = map[viewType] ?: error("ViewHolder not found!")fun byViewType(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (mapViewType(viewType)) {
CARD_VIEWS -> {
CardViewHolder(parent)
}
DETAILS_VIEW -> {
DetailsViewHolder(parent)
}
BUTTON_VIEW -> {
DetailsButtonViewHolder(parent)
}
HEADER_VIEW -> {
HeaderDetailsViewHolder(parent)
}
}
}
}
}

Next we need to create an interface that is going to be implemented in all UI Data Classes, we are going to use for the recycler view.

interface RecyclerViewItem  {    val itemType: RecyclerViewItemType
}

For every Data Class that we need to show in the adapter, it needs to override the itemType which is the enum class we have created.

data class WeatherCardViewData(
val id: String,
val title: String,
val subTitle: String,
val details: String,
val imageUrl: String = "error",
val listener: (String) -> Unit
) : RecyclerViewItem {
override val itemType: RecyclerViewItemType
get() = RecyclerViewItemType.CARD_VIEWS
}

In my example WeatherCardViewData is using the CARD_VIEWS enum which maps to the CardViewHolder.

ViewHolderGenerator

H̵e̵r̵e̵ ̵i̵s̵ ̵t̵h̵e̵ ̵r̵e̵a̵l̵ ̵m̵a̵g̵i̵c̵!̵ ̵W̵e̵ ̵a̵r̵e̵ ̵g̵o̵i̵n̵g̵ ̵t̵o̵ ̵u̵s̵e̵ ̵K̵o̵t̵l̵i̵n̵ ̵R̵e̵f̵l̵e̵c̵t̵i̵o̵n̵ ̵t̵o̵ ̵c̵r̵e̵a̵t̵e̵ ̵e̵a̵c̵h̵ ̵V̵i̵e̵w̵H̵o̵l̵d̵e̵r̵ ̵f̵o̵r̵ ̵e̵a̵c̵h̵ ̵U̵I̵ ̵D̵a̵t̵a̵ ̵i̵n̵ ̵t̵h̵e̵ ̵A̵d̵a̵p̵t̵e̵r̵.̵ ̵W̵e̵ ̵n̵e̵e̵d̵ ̵t̵o̵ ̵u̵s̵e̵ ̵p̵r̵i̵m̵a̵r̵y̵C̵o̵n̵s̵t̵r̵u̵c̵t̵o̵r̵ ̵w̵h̵i̵c̵h̵ ̵c̵a̵n̵ ̵b̵e̵ ̵f̵o̵u̵n̵d̵ ̵i̵n̵ ̵”̵o̵r̵g̵.̵j̵e̵t̵b̵r̵a̵i̵n̵s̵.̵k̵o̵t̵l̵i̵n̵:̵k̵o̵t̵l̵i̵n̵-̵r̵e̵f̵l̵e̵c̵t̵:̵$̵k̵o̵t̵l̵i̵n̵_̵v̵e̵r̵s̵i̵o̵n̵”̵.̵

We are going to use “fun byViewType(parent: ViewGroup, viewType: Int)” from our enum. We give viewType to the enum so it can map it with the ViewHolder and “parent :ViewGroup” so our ViewHolders can inflate their layout.

class DetailsButtonViewHolder(parent: ViewGroup) :
RecyclerView.ViewHolder(parent.inflate(R.layout.recycle_details_button)) {
fun bind(){
}
}

After creating the ViewHolderGenerator its time to create an Abstract class for our Adapter. In my example i am going to use ListAdapter.

abstract class GeneralRecyclerViewAdapter : ListAdapter<RecyclerViewItem, RecyclerView.ViewHolder>(TASKS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return RecyclerViewItemType.byViewType(parent, viewType)
}
override fun getItemViewType(position: Int): Int {
return getItem(position).itemType.viewType
}
companion object {
private val TAG = GeneralRecyclerViewAdapter::class.simpleName
private val TASKS_COMPARATOR = object : DiffUtil.ItemCallback<RecyclerViewItem>() {
override fun areItemsTheSame(oldItem: RecyclerViewItem, newItem: RecyclerViewItem): Boolean {
return oldItem.itemType == newItem.itemType
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: RecyclerViewItem, newItem: RecyclerViewItem): Boolean {
return oldItem == newItem
}
}
}
}

To use ListAdapter you need an Item Object and a ViewHolder. As you can see we use as Item Object the RecyclerViewItem, so every UI Data Class that inherit it can be used. For ViewHolder we use RecyclerView.ViewHolder. For ListAdapter to work correctly we need to override onCreateViewHolder and return the ViewHolder for each Item we are going to use in the Adapter.

onBindViewHolder

You can’t use an Adapter without overriding the onBindViewHolder and as you can see it’s missing from our GeneralRecyclerViewAdapter. This is because we need it only on the Adapter that its going to be used for the specific RecyclerView.

class WeatherAdapter : GeneralRecyclerViewAdapter() {    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CardViewHolder -> {
holder.bindData(getItem(position) as WeatherCardViewData)
}
}
}
}

WeatherAdapter is going to show only items that needs CardViewHolder.

class WeatherDetailsAdapter : GeneralRecyclerViewAdapter() {    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is DetailsViewHolder -> {
holder.bindData(getItem(position) as WeatherDetailsViewData)
}
is DetailsButtonViewHolder -> {
holder.bindData(getItem(position) as ButtonViewData)
}
is HeaderDetailsViewHolder -> {
holder.bindData(getItem(position) as HeaderWeatherDetailsViewData)
}
}
}
}

Here my recycler will show items that are going to use 3 differents ViewHolders. There are 3 differents UI Data Classes as well. Each one of them is implementing different RecyclerViewItemType.

Each child adapter can then communicate with the UI (either Fragment or Activity) by using delegation. For instance, declare callbacks as parameters in the adapter’s constructor.

The Android ecosystem will keep evolving at a fast pace and we have to keep up by exploring, reading and experimenting so that we can find better ways to continue building excellent Android apps.

I hope you enjoyed this article and you found it useful. If so, do give it a thumbs up, comment on it and share with with your friends.

https://twitter.com/stavris8894

Responses (1)