Generic Recycler View or how not to write boilerplate code

We all write applications and we all have lists. And the most obvious solution is RecyclerView. The implementation itself is not complicated and writing a guide on RecyclerView is no longer relevant. But there is one thing. Every time we need a list, we create a class, prescribe template methods in it, create template classes. When we have 2-3 lists, then there is nothing wrong with that. But when there are 10 or more of them, then you don’t want to do this anymore.



And faced with a problem, I began to search. Found one very interesting implementation on Kotlin. I liked it, but it lacked a few elements. After spending another couple of hours, I was able to refine it and now the implementation of the adapter takes several lines. And here I want to share it with you.



The first thing we need to do is create an adapter.



abstract class GenericAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder> { private var itemList = mutableListOf<T>() constructor() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return getViewHolder( LayoutInflater.from(parent.context) .inflate(viewType, parent, false) , viewType ) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { (holder as Binder<T>).bind(itemList[position], itemClickListener) } override fun getItemCount(): Int = itemList.size override fun getItemViewType(position: Int): Int = getLayoutId(position, itemList[position]) fun update(items: List<T>) { itemList = items.toMutableList() notifyDataSetChanged() } protected abstract fun getLayoutId(position: Int, obj: T): Int protected open fun getViewHolder(view: View, viewType: Int): RecyclerView.ViewHolder { return ViewHolderFactory.create(view, viewType) } internal interface Binder<T> { fun bind(data: T) } }
      
      





What is going on here? We create a parameterized adapter and redefine the basic template methods in it. We create the interface a parameterized Binder interface, which our ViewHolder will have to implement. In the abstract getLayoutId () method, we will set our layout.



After we create a Factory for our ViewHolder.



 object ViewHolderFactory { fun create(view: View, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { R.layout.item_data -> DataViewHolder(view) R.layout.item_other_data -> OtherDataViewHolder(view) else -> throw Exception("Wrong view type") } } class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), GenericAdapter.Binder<Data> { override fun bind(data: Data) { itemView.apply { dateTextView.text = data.dateTitle } } class OtherDataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), GenericAdapter.Binder<OtherData> { override fun bind(data: OtherData) { itemView.apply { dateTextView.text = data.dateTitle } } }
      
      





And this is how the implementation of this adapter in the fragment will look.



 private lateinit var adapter GenericAdapter<Data> protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); adapter = dataAdapter } private val dataAdapter = object : GenericAdapter<Data>() { override fun getLayoutId(position: Int, obj: Data): Int = R.layout.item_data }
      
      





Everything is cool, convenient, fast. In approximately this form, I found this implementation. But then I thought, what about clickable elements. And here is my decision.



First, create an interface



 interface OnItemClickListener<T> { fun onClickItem(data: T) }
      
      





And pass it to our binder interface
 internal interface Binder<T> { fun bind(data: T, listener: OnItemClickListener<T>?) }
      
      





And in the adapter, create an additional constructor:



  private var itemClickListener: OnItemClickListener<T>? = null constructor(listener: OnItemClickListener<T>) { itemClickListener = listener } class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), GenericAdapter.Binder<Data> { override fun bind(data: Data, listener: OnItemClickListener<Data>?) { itemView.apply { dateTextView.text = data.dateTitle setOnClickListener { listener?.onClickItem(data) } } }
      
      





What in the end we have, an adapter that is created in 3 lines and a universal interface for all kind of elements. If we don’t need to process clicks, then we simply do not pass the listener to the constructor. But that is not all.



What if we want to bind DiffUtils.Callback to our adapter.



 class GenericDiffUtil<T>( private val oldItems: List<T>, private val newItems: List<T>, private val itemDiff: GenericItemDiff<T> ) : DiffUtil.Callback() { override fun getOldListSize() = oldItems.size override fun getNewListSize() = newItems.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = itemDiff.isSame(oldItems, newItems, oldItemPosition, newItemPosition) override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = itemDiff.isSameContent(oldItems, newItems, oldItemPosition, newItemPosition) } interface GenericItemDiff<T> { fun isSame( oldItems: List<T>, newItems: List<T>, oldItemPosition: Int, newItemPosition: Int ): Boolean fun isSameContent( oldItems: List<T>, newItems: List<T>, oldItemPosition: Int, newItemPosition: Int ): Boolean }
      
      





This is what the base class for our DiffUtils looks like. Add a method to our adapter



 private var diffUtil: GenericItemDiff<T>? = null fun setDiffUtilCallback(diffUtilImpl: GenericItemDiff<T>) { diffUtil = diffUtilImpl }
      
      





And slightly modify the adapter method update ()

  fun update(items: List<T>) { if (diffUtil != null) { val result = DiffUtil.calculateDiff(GenericDiffUtil(itemList, items, diffUtil!!)) itemList.clear() itemList.addAll(items) result.dispatchUpdatesTo(this) } else { itemList = items.toMutableList() notifyDataSetChanged() } }
      
      





And so we implement our DiffUtils

 adapter.setDiffUtilCallback(dataDiffUtil) private val dataDiffUtil = object : GenericItemDiff<Data> { override fun isSame( oldItems: List<Data>, newItems: List<Data>, oldItemPosition: Int, newItemPosition: Int ): Boolean { val oldData = oldItems[oldItemPosition] val newData = newItems[newItemPosition] return oldData.id == newData.id } override fun isSameContent( oldItems: List<Data>, newItems: List<Data>, oldItemPosition: Int, newItemPosition: Int ): Boolean { val oldData = oldItems[oldItemPosition] val newData = newItems[newItemPosition] return oldData.name == newData.name && oldData.content == newData.content }
      
      





As a result, we have a simple and quite flexible implementation of template code. Convenient implementation of adapters with multiple ViewHolders. Centralized logic in one place.



Here you can see the source code .



And here you can see the original version .



All Articles