Kahibaro
Discord Login Register

13.2 Adapters and ViewHolders

Why Adapters and ViewHolders Matter

RecyclerView is designed to display large or dynamic lists efficiently. To do this, it does not know how to draw your specific list items by itself. Instead, it delegates that responsibility to an adapter, and uses ViewHolders to reuse item views as you scroll.

An adapter connects your data to the RecyclerView. It tells RecyclerView how many items exist, how to create item views, and how to bind data to those views. A ViewHolder represents a single item view and holds references to its child views, such as a TextView or an ImageView. RecyclerView reuses these ViewHolders instead of creating new ones for every item, which saves memory and improves performance.

Understanding how adapters and ViewHolders work together is essential, because almost every real list or grid you build on Android will rely on this pattern.

The Role of the Adapter

The adapter is a class that extends RecyclerView.Adapter. It acts as a bridge between the data source and the RecyclerView. It knows:

What data type it is going to show.

How to create a new item view when RecyclerView asks for one.

How to bind data for a particular position in the list to a given item view.

A very typical adapter definition looks like this:

class PersonAdapter(
    private val items: List<Person>
) : RecyclerView.Adapter<PersonAdapter.PersonViewHolder>() {
    // ...
}

The generic type PersonAdapter.PersonViewHolder tells RecyclerView what kind of ViewHolder this adapter will use. The adapter must implement three key methods:

onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder
onBindViewHolder(holder: PersonViewHolder, position: Int)
getItemCount(): Int

These methods are the core of the adapter behavior.

In every RecyclerView adapter you must correctly implement getItemCount, onCreateViewHolder, and onBindViewHolder. If any of these are wrong, your list will not display correctly.

Implementing getItemCount

getItemCount tells RecyclerView how many items should be displayed. RecyclerView uses this information to know when to stop scrolling and how many times it should request binding operations.

If your adapter holds a list, the implementation is usually very simple:

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

If this returns 0, RecyclerView will not show any items. If it returns a number larger than your list size, RecyclerView will try to bind positions that do not exist, which will cause errors. Always make sure that the returned value matches the number of elements in your underlying data.

Creating ViewHolders with onCreateViewHolder

onCreateViewHolder is called when RecyclerView needs a new item view to display an element. This typically happens only a limited number of times. After that, existing ViewHolders are reused for different positions.

Inside onCreateViewHolder you:

Inflate your item layout XML into a View object.

Create an instance of your ViewHolder class, passing the inflated view.

For example, given an item layout file named item_person.xml, you might write:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
    val view = LayoutInflater.from(parent.context)
        .inflate(R.layout.item_person, parent, false)
    return PersonViewHolder(view)
}

The parent parameter is the RecyclerView itself. Passing parent and false to inflate ensures that the correct layout parameters are applied to the item view, but the view is not immediately attached to the parent.

viewType is used for lists that have different item layouts. For simple lists you can ignore it and use a single layout.

Understanding the ViewHolder Pattern

A ViewHolder is a class that holds references to the views of a single list item. It usually extends RecyclerView.ViewHolder and receives the root item view in its constructor.

Basic ViewHolder implementation for a layout with a TextView might look like this:

class PersonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val nameTextView: TextView = itemView.findViewById(R.id.textName)
}

The main idea is that you perform the findViewById calls once when the ViewHolder is created. Then, every time the ViewHolder is reused for a different position, you update the views directly, without searching the view hierarchy again.

This pattern significantly improves performance for lists with many items.

Binding Data in onBindViewHolder

onBindViewHolder is called when a ViewHolder should display data for a given position in the list. At this point the ViewHolder already has references to its child views, so you only need to set the content based on your data.

For a list of Person objects, you might write:

override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
    val person = items[position]
    holder.nameTextView.text = person.name
}

RecyclerView will call this method repeatedly as the user scrolls. The same ViewHolder instance can be used with different position values over time. You should not store the position inside the ViewHolder, because it changes. Always use the position parameter or holder.adapterPosition when you need to know which item is being bound.

If your item view has more than one widget, you update each one here, for example setting images, descriptions, or click listeners that depend on the data.

Connecting Adapter and ViewHolder Types

The adapter and the ViewHolder are tightly linked through the adapter's generic type. When you declare:

class PersonAdapter(
    private val items: List<Person>
) : RecyclerView.Adapter<PersonAdapter.PersonViewHolder>() {
    class PersonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val nameTextView: TextView = itemView.findViewById(R.id.textName)
    }
    // onCreateViewHolder, onBindViewHolder, getItemCount...
}

you tell RecyclerView that:

onCreateViewHolder must return PersonViewHolder.

onBindViewHolder will receive a PersonViewHolder.

This structure ensures type safety. Inside onBindViewHolder you can access holder.nameTextView without casting, because Kotlin already knows it is a PersonViewHolder.

View Recycling and Why It Matters

RecyclerView does not create a new ViewHolder for every item in the data set. Instead, it maintains a pool of ViewHolders and recycles them as you scroll. When a ViewHolder scrolls off screen, RecyclerView can reuse it for a different position, which is why it calls onBindViewHolder again with a new position.

From your perspective, this means:

You should always fully configure the item in onBindViewHolder, even if you think a view property is already set. A recycled ViewHolder may carry old state from a previous binding if you do not overwrite it.

You should avoid assuming that the position associated with a ViewHolder is fixed. The same ViewHolder object will represent different positions as the user scrolls.

To see this in code, consider this simplified flow:

  1. onCreateViewHolder creates a ViewHolder for item index 0.
  2. onBindViewHolder binds data at position 0.
  3. The user scrolls. That ViewHolder moves off screen.
  4. RecyclerView reuses the same ViewHolder.
  5. onBindViewHolder binds data at position 10 to the same ViewHolder.

Binding must handle this reuse correctly. If you show or hide views based on data, make sure you reset visibility or other properties every time you bind.

Handling Dynamic Data in Adapters

In many real apps, the data shown in a RecyclerView can change. You might load new items from the network, remove items, or allow the user to reorder the list. When the data set changes, you must notify the adapter so that RecyclerView can update the visible items.

At the simplest level, after you change the items list inside your adapter, you can call:

notifyDataSetChanged()

This tells RecyclerView to refresh all visible items. It is easy to use, but not very efficient for large lists since it causes all items to be redrawn.

For more precise updates, you can use methods like:

notifyItemInserted(position)
notifyItemRemoved(position)
notifyItemChanged(position)
notifyItemRangeInserted(startPosition, itemCount)

These inform RecyclerView of exactly what changed, so it can animate and update only the affected items.

After changing the underlying data of your adapter you must call an appropriate notify... method. If you forget to do this, RecyclerView will not update and the screen will still show old data.

Handling Multiple View Types

Sometimes one RecyclerView must display different types of items, for example headers and regular rows, or messages sent and messages received. In these cases you can override getItemViewType in the adapter.

A simple pattern for two item types might be:

companion object {
    private const val TYPE_HEADER = 0
    private const val TYPE_ITEM = 1
}
override fun getItemViewType(position: Int): Int {
    return if (position == 0) TYPE_HEADER else TYPE_ITEM
}

Then, in onCreateViewHolder, you decide which layout to inflate based on viewType:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return if (viewType == TYPE_HEADER) {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_header, parent, false)
        HeaderViewHolder(view)
    } else {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_row, parent, false)
        RowViewHolder(view)
    }
}

Each ViewHolder class corresponds to one layout type. In onBindViewHolder, you cast the holder to the appropriate type based on viewType or by using when with is.

This approach allows complex lists with mixed content while still benefiting from the same recycling mechanism.

ViewBinding and ViewHolders

Manually calling findViewById in ViewHolders can be repetitive and error prone. With ViewBinding enabled for your module, you can generate a binding class for each layout and use it inside the ViewHolder.

Instead of:

class PersonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val nameTextView: TextView = itemView.findViewById(R.id.textName)
}

you can write:

class PersonViewHolder(
    val binding: ItemPersonBinding
) : RecyclerView.ViewHolder(binding.root)

Then in onCreateViewHolder:

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

And in onBindViewHolder:

override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
    val person = items[position]
    holder.binding.textName.text = person.name
}

This approach removes direct dependency on findViewById, and the compiler helps ensure that you use only the views that exist in the layout.

Common Mistakes and How to Avoid Them

A few frequent issues appear when beginners implement adapters and ViewHolders.

One mistake is to inflate layouts incorrectly in onCreateViewHolder, for example by passing null as the parent:

// Problematic: missing parent, layout parameters may be wrong
LayoutInflater.from(parent.context).inflate(R.layout.item_person, null)

Always provide parent and false for the attachToRoot parameter, so that the layout parameters are inherited correctly.

Another common error is to store the position in the ViewHolder and reuse it later, for instance inside click listeners. Instead of saving the initial position, obtain the latest position using adapterPosition inside the click handler, because items can move or be removed.

Finally, avoid performing heavy work in onBindViewHolder such as long computations or blocking network calls. Binding should be quick, since it runs often and directly affects scroll smoothness. If you need to load images or data from the network, use background tasks and dedicated image loading libraries, then update the ViewHolder when the data is ready.

Bringing It All Together

An adapter and its ViewHolder work together to turn your data into efficient, scrollable lists. The adapter knows where the data is and how many items exist. The ViewHolder knows how to display one item view. RecyclerView coordinates both, reuses ViewHolders to save resources, and asks the adapter to bind data whenever needed.

Once you are comfortable implementing getItemCount, onCreateViewHolder, and onBindViewHolder, and you understand how ViewHolders cache child view references, you will be able to build flexible lists that handle a wide variety of layouts and data sources.

Views: 1

Comments

Please login to add a comment.

Don't have an account? Register now!