Writing a RecyclerView Adapter in Kotlin (KAD 16)

An interesting way to see how Kotlin simplifies your life is by creating a RecyclerView Adapter.

You’ll see that the code can be organized in such a way that its reading is easier and it avoids redundant code.

RecyclerView Adapter in Kotlin

We’ll create an Adapter that will set a title and an image into a cell.

It’ll be a very simple adapter in which we won’t allow the modification of the items. If we want new data, we will create a new adapter and set it to the RecyclerView.

If all this passionate you as to me, I encourage you to sign up for my free training where I will tell you everything you need to learn about how to create your Android Apps in Kotlin from scratch.

The model

We’ll use a very simple model too, which only needs an identifier, the title and the url of the image.

We’ll use a data class, which if you remember we saw a few articles ago:

data class Item(val id: Long, val title: String, val url: String)

With this we already have a class with its constructor, its immutable properties, and other useful functions like equals or hashCode implemented.

The Adapter

The Adapter’s structure would be the following, self-generating the necessary methods:

class MyAdapter : RecyclerView.Adapter() {
     
    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
    }

    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
    }
f    override fun getItemCount(): Int {
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}

You’ll see that I’ve created a ViewHolder class, which extends from the original.

This is because the Adapter needs an implementation of the original abstract class.

Also, there are some elements marked as nullable. This is because if the library isn’t properly annotated with the annotations @Nullable and @NonNull, Kotlin has no way of knowing if nulls are allowed, and it lets us decide.

If we self-generate the methods, by default it’ll take for granted that the values are nullable.

But studying the support library a bit, we know that those values aren’t null, so we can remove it:

class MyAdapter : RecyclerView.Adapter() {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    }

    override fun getItemCount(): Int {
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}

The constructor

The adapter needs to receive the items and a listener by parameter. The result is something like this:

class MyAdapter(val items: List, val listener: (Item) -> Unit)

The implementation of the methods is very easy. I’m using an extension function that I’d created in a previous article to inflate the view:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(parent.inflate(R.layout.view_item))

override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener)

override fun getItemCount() = items.size

There are three methods can be implemented with the contracted form, obtaining the previous result. In three lines we have all the adapter implemented.

Now go for the ViewHolder implementation.

The ViewHolder

The ViewHolder will assign the values from the model to their corresponding views:

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    fun bind(item: Item, listener: (Item) -> Unit) = with(itemView) {
        itemTitle.text = item.title
        itemImage.loadUrl(item.url)
        setOnClickListener { listener(item) }
    }
}

Everything we have here has already been seen in other articles: the with function, the loadUrl extension function for ImageView, the access to Views using Kotlin Android Extensions and the mapping of the click listener.

As mentioned by Nabil in the comments, this way to use Kotlin Android Extensions in Views has some hidden costs, and is not always optimal. Take a look at the Kotlin Android Extensions article in this blog, where I show a new way to use cache on ViewHolders, and also this other article which talks about this hidden costs I’m mentioning. In any case, my recommendation is to test before optimizing.

Adapter assignment

There’s only one thing left: assign the adapter to the view

recycler.layoutManager = GridLayoutManager(this, 2)
recycler.adapter = MyAdapter(items) {
    toast("${it.title} Clicked")
}

The final function is listener, which receives an item. The code will simply print the title of the item you click on.

Conclusion

As simple as that is to implement a RecyclerView Adapter in Kotlin.

Using a few of the tools we have learned so far, we’ve simplified the code to the minimum.

Cheer up and come to my free training. I will tell you everything you need to learn about how to create your own Android Apps in Kotlin from scratch. Discover everything about the language of the future.

23 thoughts on “Writing a RecyclerView Adapter in Kotlin (KAD 16)”

    1. Ey thanks! Yeah, it could probably be more efficient, but it’s just cheap allocation. Setting an image to an ImageView or a text to a TextView also involves allocations, so don’t think there will be much difference in real world.

  1. Thanks for the simple tutorial. You can however improve this. Accessing to itemView.itemTitle is actually performing itemView.findViewById(R.id.itemTitle) and this is exactly what you want to avoid in a ViewHolder. Instead you can do this

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    val title: TextView = itemView.itemTitle
    val image: ImageView = itemView.itemImage

    fun bind(item: Item, listener: (Item) -> Unit) = with(itemView) {
    title.text = item.title
    image.loadUrl(item.url)
    setOnClickListener { listener(item) }
    }
    }

    Check this https://proandroiddev.com/the-costs-of-kotlin-android-extensions-6809e2b32b13

    Thanks

    1. That’s commented in a previous article, yeah. I agree with you that in some cases it’s a good idea to cache the views, but in simple views this won’t be an issue. findViewById is not a performance issue in many situations nowadays.

      1. Yes you are correct…. perhaps noting them here would be a better help for people coming here firsttime…. Thanks

  2. Thank you for a great job!
    I think that bind function in the ViewHolder is a violation of Single Responsibility principle.
    ViewHolder should not do anything but holds the view.

    I suggest binding in onBindViewHolder method.
    If you face complex adapter with a lot of item view types, it is a good idea to use Delegation.
    For every type of view define delegate which will create and bind ViewHolder.

    1. Well, I think the adapter is violating the SRP in many more ways that the ViewHolder. For me, holding a view is not a responsibility, because it basically does nothing. I prefer to use it to bind the data to the view, but I can see your point too. In any case, I usually prefer to do custom views, and those views would hold the responsibility of binding the model. This is just a simple example.

  3. asdevelopments

    I’m so sick of adapters and viewHolders so I wrote this single Adapter class to be used in all my recyclers

    Note: It can be used as simple as that:
    recyclerView.adapter = RecyclerViewKAdapter(R.layout.item_layout, { kHolder, _, item, _ ->
    kHolder.iconImageView.setImageResource(item.icon)
    kHolder.nameTextView.setText(item.name)

    })

    class RecyclerViewKAdapter(private val layoutRes: Int, private val onBindKHolder: (holder: KHolder, position: Int, model: MODEL) -> Unit, data: List = ArrayList()) : RecyclerView.Adapter() {

    /**
    * Adapter data
    */
    var data: List = ArrayList()
    set(value) {
    field = value
    notifyDataSetChanged()
    }

    override fun onBindViewHolder(holder: KHolder, position: Int) {
    onBindKHolder.invoke(holder, position, data[position])
    }

    override fun getItemCount(): Int = data.size

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): KHolder = KHolder(LayoutInflater.from(parent?.context).inflate(layoutRes, parent, false))

    /**
    * @param containerView root item view
    */
    class KHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer
    }

    /**
    * recycler view data
    */
    @Suppress(“UNCHECKED_CAST”)
    var RecyclerView.data: List
    set(value) {
    require(adapter != null && adapter is RecyclerViewKAdapter)
    (adapter as RecyclerViewKAdapter).data = value
    }
    get() = (adapter as RecyclerViewKAdapter).data

  4. Hello, thanks for the tutorial. I have bought your book but didn’t find this article there, so I just wanted to know what is the Item (capital I) in “fun bind(item: Item, listener: (Item) -> Unit)” ? Thank you.

    1. Hey Andre! The definition of that class is at the very beginning of the article. It’s a random class that represents the items that the adapter is gonna render.

  5. I understand the importance of inmutabillity but this makes me doubt, lets say you need to update a single item of the list how would you handle this

  6. Got issue with recyclerview getItemCount. I can get the count of item in a recyclerview but the problem is when i remove an item in the recyclerview the count is not changing. How can I make the item count updated?

Comments are closed.