119

Can I use ViewBindings to replace findViewById in this typical RecyclerView.Adapter initialization code? I can't set a binding val in the object as the ViewHolders are different per cell.

class CardListAdapter(private val cards: LiveData<List<Card>>) : RecyclerView.Adapter<CardListAdapter.CardViewHolder>() {

    class CardViewHolder(val cardView: View) : RecyclerView.ViewHolder(cardView)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
        val binding = CardBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return CardViewHolder(binding.root)
    }

    override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
        val title = holder.cardView.findViewById<TextView>(R.id.title)
        val description = holder.cardView.findViewById<TextView>(R.id.description)
        val value = holder.cardView.findViewById<TextView>(R.id.value)
        // ...
    }
A1m
  • 2,897
  • 2
  • 24
  • 39
  • 1
    "the ViewHolders are different per cell" -- so have each `ViewHolder` do its own binding by means of its dedicated binding object. Pass the `CardBinding` to the `CardViewHolder` constructor so it has that binding object. IOW, this is no different than using data binding with `RecyclerView`, and that combination has been used for a few years. – CommonsWare Feb 26 '20 at 23:02
  • 1
    I found you can also use `CardBinding.bind(holder.cardView)`. I wonder whether the `bind` method has worse performance? – A1m Feb 27 '20 at 00:07
  • Checkout my blog on viewbinding I explained anti-pattern and patterns regarding binding in adapter fragment and activity, it's in depth with internals as well [Androidbites|Viewbinding](https://chetangupta.net/viewbinding/) – Chetan Gupta Dec 08 '20 at 15:22
  • yes you can use see my answer – Bharat Lalwani Feb 17 '22 at 13:14

9 Answers9

227

What you need to do is pass the generated binding class object to the holder class constructor. In below example, I have row_payment XML file for RecyclerView item and the generated class is RowPaymentBinding so like this

class PaymentAdapter(private val paymentList: List<PaymentBean>) : RecyclerView.Adapter<PaymentAdapter.PaymentHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentHolder {
        val itemBinding = RowPaymentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return PaymentHolder(itemBinding)
    }

    override fun onBindViewHolder(holder: PaymentHolder, position: Int) {
        val paymentBean: PaymentBean = paymentList[position]
        holder.bind(paymentBean)
    }

    override fun getItemCount(): Int = paymentList.size

    class PaymentHolder(private val itemBinding: RowPaymentBinding) : RecyclerView.ViewHolder(itemBinding.root) {
        fun bind(paymentBean: PaymentBean) {
            itemBinding.tvPaymentInvoiceNumber.text = paymentBean.invoiceNumber
            itemBinding.tvPaymentAmount.text = paymentBean.totalAmount
        }
    }
}

Also, make sure you pass the root view to the parent class of Viewholder like this RecyclerView.ViewHolder(itemBinding.root) by accessing the passed binding class object.

Steven Benitez
  • 10,936
  • 3
  • 39
  • 50
Somesh Kumar
  • 8,088
  • 4
  • 33
  • 49
  • 11
    Very clean. Thank you. An improvement to the bind function would be: `fun bind(paymentBean: PaymentBean) = with(itemBinding) { tvPaymentInvoiceNumber.text = paymentBean.invoiceNumber tvPaymentAmount.text = paymentBean.totalAmount }` ...to avoid repeatedly writing of `itemBinding`. – robsn Dec 25 '20 at 13:21
  • 2
    Or alternatively to avoid repeating 'itemBinding' you could use 'itemBinding.apply { ... }' – robsn Dec 25 '20 at 13:37
  • 2
    @robsn yeah, we can do that.. but that wasn't the intention of this answer and I wanted to make it easy for the beginners that's why I didn't use scope function. – Somesh Kumar Dec 31 '20 at 09:44
  • If you are using databinding, what's the purpose of doing `itemBinding.tvPaymentInvoiceNumber.text = paymentBean.invoiceNumber` and so forth? You can just do `binding.executePendingBindings()` – Chisko Feb 16 '21 at 22:51
  • @Chisko I am not using DataBinding in this example and tbh I am not a fan of data-binding. – Somesh Kumar Feb 17 '21 at 05:39
  • 2
    Why can't you leave the names as default when you install Recyclerview instead of paymentHolder paymentWhatever – Eduard Unruh Sep 26 '21 at 15:07
  • @EduardUnruh What should be used here instead of PaymentWhatever? – Somesh Kumar Sep 27 '21 at 05:15
  • Hey, this is very clean! thank you :-) – Abhijith Brumal Nov 12 '21 at 07:09
  • what if I used databinding utils isn't better? – Moustafa EL-Saghier Mar 06 '22 at 08:32
37

Attach the binding to the ViewHolder instead of the View

class CardViewHolder(val binding: CardBinding) : RecyclerView.ViewHolder(binding.root)

You pass the binding, the binding passes binding.root to RecyclerView.ViewHolder(binding.root)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
    val binding = CardBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    return CardViewHolder(binding)
}

Then access anywhere with:

holder.binding.title
Stonz2
  • 6,306
  • 4
  • 44
  • 64
A1m
  • 2,897
  • 2
  • 24
  • 39
  • you should create the binding in `onCreateViewHolder` instead of `onBindViewHolder`. – EpicPandaForce Apr 20 '20 at 15:52
  • (because this answer currently works-around the idea of the `ViewHolder` only looking up references in `onCreateViewHolder` -> `ViewHolder` constructor, and instead runs `findViewById` for each `onBindViewHolder` call, eliminating the ViewHolder's ability to actually "hold views" - the other answer is correct: https://stackoverflow.com/a/60427658/2413303) – EpicPandaForce Apr 20 '20 at 15:59
  • You are entirely correct. That's what my second option is. The first one shouldn't be used as per your reasoning. Thanks! – A1m Apr 21 '20 at 02:48
  • You might want to edit the answer itself to reflect that. – EpicPandaForce Apr 21 '20 at 03:16
11

I wrote a simple and reusable one:

class ViewBindingVH constructor(val binding: ViewBinding) :
    RecyclerView.ViewHolder(binding.root) {
    
    companion object {
        inline fun create(
            parent: ViewGroup,
            crossinline block: (inflater: LayoutInflater, container: ViewGroup, attach: Boolean) -> ViewBinding
        ) = ViewBindingVH(block(LayoutInflater.from(parent.context), parent, false))
    }
}


class CardAdapter : RecyclerView.Adapter<ViewBindingVH>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewBindingVH {
        return ViewBindingVH.create(parent, CardBinding::inflate)
    }

    override fun onBindViewHolder(holder: ViewBindingVH, position: Int) {
        (holder.binding as CardBinding).apply {
            //bind model to view
            title.text = "some text"
            descripiton.text = "some text"
        }
    }

}
Alan W.
  • 4,566
  • 2
  • 15
  • 26
  • will this cause the inflater to be done once? – Moustafa EL-Saghier Mar 01 '22 at 10:55
  • @Moustafa EL-Saghier No, It's just a little decoration of ViewHolder. – Alan W. Mar 04 '22 at 06:21
  • Do you know any way the inflater works once for multiple views types of same view holder – Moustafa EL-Saghier Mar 04 '22 at 15:43
  • @Moustafa EL-Saghier I haven't understood what you mean, a view holder should coresspond just one view type. – Alan W. Mar 06 '22 at 03:47
  • I've different view types with different view holders in same recycler but there's a view type repeating multiple, so I ask any way 6to make the views holder inflated once if the view type repeated? – Moustafa EL-Saghier Mar 06 '22 at 08:19
  • If I haven't misunderstood you, what you want is that viewholder of repeated view type should create just once. Actually,it depends configuraiton of recyclerview.Each view type has one cached viewholder defaultly, when a item of this type is out of recyclerview, it's viewholder will recycled and reused to binding new data. – Alan W. Mar 06 '22 at 08:42
  • yes i know that but, my issue is there's a section called Products and it's shown many in home recycler view so was woundering if i can make the onCreateViewHolder being called once for the multiple view type or not – Moustafa EL-Saghier Mar 06 '22 at 10:04
  • 1
    If what you mean is The products scetion has different view types, no. Recycerview will check if there is a viewholder for each view type in cache pool.If not, onCreateViewHolder will invoked. – Alan W. Mar 08 '22 at 03:04
6

You may use view-binding like this :

package com.example.kotlinprogramming.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlinprogramming.data.HobbiesData
import com.example.kotlinprogramming.databinding.ItemHobbieBinding

class HobbiesAdapter(var context: Context, var hobbiesList: List<HobbiesData>) :
RecyclerView.Adapter<HobbiesAdapter.HobbiesViewHolder>() {


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HobbiesViewHolder {
    val view = ItemHobbieBinding.inflate(LayoutInflater.from(context) , parent,false)
    return HobbiesViewHolder(view)
}

override fun onBindViewHolder(holder: HobbiesViewHolder, position: Int) {
    val hobbie = hobbiesList.get(position)
    holder.viewBinding.tvHobbie.text = hobbie.title
}

inner class HobbiesViewHolder(var viewBinding: ItemHobbieBinding) : RecyclerView.ViewHolder(viewBinding.root) {
}

override fun getItemCount(): Int {
    return hobbiesList.size
}

}

Here is item_hobbies.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_margin="12dp"
android:layout_height="wrap_content"
>
<TextView
    android:id="@+id/tvHobbie"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:gravity="center"
    android:textSize="30sp"
    tools:text="Hobbie1"
    />
</androidx.cardview.widget.CardView>
Sathish Gadde
  • 1,453
  • 16
  • 28
5

If you're ok with reflection, I have a much easier way to do this. Just call ViewGroup.toBinding() then you can get the binding object you want. But since we're talk about reflection, remember you have to modify your proguard-rule to make it work even for proguard.

// inside adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    return MainViewHolder(parent.toBinding())
}

// ViewHolder
class MainViewHolder(private val binding: AdapterMainBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(data: String) {
        binding.name.text = data
    }
}

// The magic reflection can reused everywhere.
inline fun <reified V : ViewBinding> ViewGroup.toBinding(): V {
    return V::class.java.getMethod(
        "inflate",
        LayoutInflater::class.java,
        ViewGroup::class.java,
        Boolean::class.java
    ).invoke(null, LayoutInflater.from(context), this, false) as V
}

I put all this into an open source project you can take a look as well. Not only for Adapter usage but also include Activity and Fragment. And do let me know if you have any comment. Thanks.

https://github.com/Jintin/BindingExtension

Jintin
  • 1,426
  • 13
  • 22
0

just pass your model calss into xml and set these data into xml this code look fine and add a method where you add these data into binding like you don,t need to fine the id for this

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.setData(listData[position])

}


fun setData(model: ListData) {
        with(binding) {
            data = model
            executePendingBindings()
        }
    }
Amit pandey
  • 1,149
  • 1
  • 4
  • 15
0

You may use data binding like this.

class CardListAdapter(
        private val mActivity: FragmentActivity?
    ) :
        RecyclerView.Adapter<RecyclerView.ViewHolder>() {
         private var mCustomLayoutBinding: CustomLayoutBinding? = null

          inner class MyViewHolder(val mBinding: CustomLayoutBinding) :
            RecyclerView.ViewHolder(mBinding.getRoot())
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            if (layoutInflater == null)
                layoutInflater = LayoutInflater.from(parent.context)

            var viewHolder: RecyclerView.ViewHolder? = null
            val inflater = LayoutInflater.from(parent.context)

       viewHolder = getViewHolder(parent, inflater)
                 return viewHolder!!
            }
            private fun getViewHolder(
            parent: ViewGroup,
            inflater: LayoutInflater
        ): RecyclerView.ViewHolder {
            mCustomLayoutBinding =
                DataBindingUtil.inflate(inflater, R.layout.custom_layout, parent, false)
            return MyViewHolder(this!!.mAssistanceLogCustomLayoutBinding!!)
        }
          override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            val taskModal = mArrayList.get(position)
             holder.mBinding.txtTitle.setText(taskModal.title)
              }
              override fun getItemCount(): Int {
            return assistanceArrayList.size
        }

        override fun getItemId(position: Int): Long {
            return position.toLong()
        }

        override fun getItemViewType(position: Int): Int {
            return position
        }
        }
Komal
  • 328
  • 3
  • 15
0

I took what Alan W. did and added Generics to it.

class ViewBindingVH <VB: ViewBinding> constructor(val binding: VB) :
RecyclerView.ViewHolder(binding.root) {

companion object {
    inline fun <VB: ViewBinding> create(
        parent: ViewGroup,
        crossinline block: (inflater: LayoutInflater, container: ViewGroup, attach: Boolean) -> VB
    ) = ViewBindingVH<VB>(block(LayoutInflater.from(parent.context), parent, false))
}}

The implementation is very easy and you avoid the casting on the adapter class:

class PlayerViewHolder : ListAdapter<Rate, ViewBindingVH<ChartItemBinding>>(RateDiff) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewBindingVH<ChartItemBinding> =
    ViewBindingVH.create(parent, ChartItemBinding::inflate)


override fun onBindViewHolder(holder: ViewBindingVH<ChartItemBinding>, position: Int) {
    val item = currentList[position]
    holder.binding.apply {
        
    }
}}

object RateDiff: DiffUtil.ItemCallback<Rate>(){
override fun areContentsTheSame(oldItem: Rate, newItem: Rate): Boolean {
    return oldItem == newItem
}
override fun areItemsTheSame(oldItem: Rate, newItem: Rate): Boolean {
    return oldItem == newItem
}}
cigien
  • 57,834
  • 11
  • 73
  • 112
itzik pichhadze
  • 101
  • 1
  • 3
  • I considered using generics, but I gave up it because of mulit view types. generics limits your adapter to just one view type. – Alan W. Feb 07 '22 at 03:42
0
abstract class BaseRecyclerViewAdapter<T : Any, VB : ViewBinding>(
     private var dataList: ArrayList<T>) 
: RecyclerView.Adapter<BaseRecyclerViewAdapter.MyViewViewHolder<VB>>() 
{   
      protected var bindingInterface: GenericSimpleRecyclerBindingInterface<T, VB>? = null

   class MyViewViewHolder<VB : ViewBinding>(val viewBinding: VB) :
    RecyclerView.ViewHolder(viewBinding.root) {
        fun <T : Any> bind(
            item: T,
            position: Int,
            bindingInterface: GenericSimpleRecyclerBindingInterface<T, VB>
            ) = bindingInterface.bindData(item, position, viewBinding)
        }

    @SuppressLint("NotifyDataSetChanged")
    fun updateList(list: ArrayList<T>) {
       dataList.clear()
       dataList.addAll(list)
       notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):MyViewViewHolder<VB> {
       val view = inflateView(LayoutInflater.from(parent.context))
       return MyViewViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewViewHolder<VB>, position: Int) {
       val item = dataList[position]
       holder.bind(item, position, bindingInterface!!)
    }

    override fun getItemCount(): Int = dataList.size

    abstract fun inflateView(inflater: LayoutInflater): VB
}

interface GenericSimpleRecyclerBindingInterface<T : Any, VB : ViewBinding> {
     fun bindData(item: T, position: Int, viewBinding: VB)
}
Bharat Lalwani
  • 1,277
  • 15
  • 17