RecyclerView

Android Development Fundamentals
Course 1 ยท Chapter 5 ยท RecyclerView

๐Ÿ“œ RecyclerView

RecyclerView is Android's tool for scrollable lists โ€” a contact list, a feed, search results. Its defining trait is right in the name: instead of creating a View for every single item up front, it recycles a small, fixed number of item Views as the user scrolls, reusing off-screen ones instead of endlessly allocating new ones. This chapter covers the adapter pattern that makes that possible, ViewHolder, efficient updates with DiffUtil, and handling clicks on individual items.

๐Ÿงฉ The Adapter Pattern

RecyclerView itself knows nothing about your data โ€” an Adapter is the bridge between a data source (a List<T>, most often) and the actual item Views on screen, implemented with three required overrides:

data class Contact(val name: String, val phone: String) class ContactAdapter(private val contacts: List<Contact>) : RecyclerView.Adapter<ContactAdapter.ContactViewHolder>() { class ContactViewHolder(val binding: ItemContactBinding) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder { val binding = ItemContactBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return ContactViewHolder(binding) } override fun onBindViewHolder(holder: ContactViewHolder, position: Int) { val contact = contacts[position] holder.binding.nameText.text = contact.name holder.binding.phoneText.text = contact.phone } override fun getItemCount() = contacts.size }

onCreateViewHolder โ€” Called Rarely

Inflates a brand-new item layout and wraps it in a ViewHolder. Only called enough times to fill the visible screen plus a small buffer โ€” not once per data item, which is the entire performance idea behind RecyclerView.

onBindViewHolder โ€” Called Constantly

Takes an already-created ViewHolder and fills it with data for a specific position. This runs every time a recycled View scrolls back on screen with new data โ€” it should stay cheap, since it fires very frequently during scrolling.

๐Ÿ—‚๏ธ ViewHolder โ€” Caching View Lookups

ViewHolder exists purely to hold references to one item's Views (via view binding, per Chapter 3) so they're looked up once, at creation, rather than re-searched on every bind. Without it, every scroll frame would repeat the equivalent of a fresh findViewById for every visible item, which used to be a genuine performance problem in Android's early list-view APIs.

// item_contact.xml โ€” one row's layout, referenced by ItemContactBinding above <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp"> <TextView android:id="@+id/nameText" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/phoneText" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>

๐Ÿ”Œ Wiring It Up in an Activity

val contacts = listOf( Contact("Alice", "555-0101"), Contact("Bob", "555-0102") ) binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = ContactAdapter(contacts)

A layoutManager is a separate, required piece โ€” it decides how items are arranged (LinearLayoutManager for a simple vertical or horizontal list, GridLayoutManager for a grid). RecyclerView deliberately splits "how data becomes Views" (the Adapter) from "how those Views are positioned" (the LayoutManager) โ€” two independent, swappable concerns.

โšก DiffUtil โ€” Efficient List Updates

Calling notifyDataSetChanged() after a data change tells the Adapter "something changed, redraw everything" โ€” correct, but wasteful, and it kills any scroll-position-preserving item animations. DiffUtil instead computes the minimal set of actual changes between an old list and a new one:

class ContactDiffCallback( private val oldList: List<Contact>, private val newList: List<Contact> ) : DiffUtil.Callback() { override fun getOldListSize() = oldList.size override fun getNewListSize() = newList.size override fun areItemsTheSame(oldPos: Int, newPos: Int) = oldList[oldPos].phone == newList[newPos].phone // same underlying entity? (an ID, typically) override fun areContentsTheSame(oldPos: Int, newPos: Int) = oldList[oldPos] == newList[newPos] // same VALUES? (data class equals(), from Kotlin Fundamentals Ch4) }

areItemsTheSame answers "is this the same real-world contact?" (usually by a stable ID) โ€” areContentsTheSame answers "has anything about it actually changed?" DiffUtil then tells the Adapter to animate exactly the items that were added, removed, moved, or changed, leaving everything else alone.

DiffUtil vs React's Key-Based Reconciliation

ReactRecyclerView + DiffUtil
Identity checkkey prop on each list itemareItemsTheSame()
Change checkShallow prop comparison (or memo)areContentsTheSame()
ResultMinimal virtual DOM diff appliedMinimal RecyclerView item animations applied
โš  ListAdapter โ€” DiffUtil Without the Boilerplate

In practice, most real code extends ListAdapter<Contact, ContactViewHolder> instead of plain RecyclerView.Adapter โ€” it wraps DiffUtil internally (via a smaller DiffUtil.ItemCallback) and exposes a single submitList(newList) call that handles the diffing and updating automatically. The manual DiffUtil.Callback above is worth understanding once, precisely so ListAdapter's automatic version doesn't feel like unexplained magic.

๐Ÿ‘† Handling Clicks on List Items

Click handling is set up per-item inside onBindViewHolder, most cleanly by passing a lambda into the Adapter's constructor โ€” a direct callback-parameter pattern, same shape as passing a lambda to map/filter back in Kotlin Fundamentals Chapter 6:

class ContactAdapter( private val contacts: List<Contact>, private val onContactClick: (Contact) -> Unit ) : RecyclerView.Adapter<ContactAdapter.ContactViewHolder>() { override fun onBindViewHolder(holder: ContactViewHolder, position: Int) { val contact = contacts[position] holder.binding.nameText.text = contact.name holder.binding.root.setOnClickListener { onContactClick(contact) } } // ...onCreateViewHolder, getItemCount unchanged... } // In the Activity: binding.recyclerView.adapter = ContactAdapter(contacts) { clickedContact -> Log.d("MainActivity", "Clicked: ${clickedContact.name}") }

๐Ÿ’ป Coding Challenges

Challenge 1: A Basic RecyclerView List

Build a RecyclerView showing a list of at least 5 data class Product(val name: String, val price: Double) items, with an item layout showing both fields. Write the Adapter and ViewHolder, and wire it up in the Activity with a LinearLayoutManager.

Goal: Practice the full Adapter/ViewHolder/getItemCount setup from scratch.

โ†’ Solution

Challenge 2: Click Handling

Extend Challenge 1's Adapter to accept an onProductClick: (Product) -> Unit lambda in its constructor, set on each item's root view in onBindViewHolder. In the Activity, log the clicked product's name via the passed-in lambda.

Goal: Practice the constructor-lambda click-handling pattern.

โ†’ Solution

Challenge 3: A DiffUtil.Callback

Write a ProductDiffCallback implementing DiffUtil.Callback for two List<Product>, using the product name as the stable identity for areItemsTheSame, and full data class equality for areContentsTheSame. In a comment, describe what would happen (in terms of item animations) if two lists differed by one product's price changing.

Goal: Practice writing DiffUtil.Callback correctly, understanding the distinct roles of its two comparison methods.

โ†’ Solution

๐Ÿ’ก The Same Recycling Idea Applies to Every Scrollable List

Every long list in a real Android app โ€” a chat history, an e-commerce catalog, a social feed โ€” uses this exact same adapter/ViewHolder shape underneath, no matter how visually different the items look. Once this pattern is genuinely comfortable, most list-related work becomes "design this one item's layout," not "figure out list infrastructure again."

๐ŸŽฏ What's Next

Next chapter: Fragments โ€” fragment lifecycle, the fragment manager, and communicating with the hosting Activity.