RecyclerView & Adapters — Displaying Lists of Data
In almost every Android app, you need to display a list of items — contacts, messages, products, posts, etc. RecyclerView is the modern, powerful, and efficient way to display large lists of data in Android. It replaces the older ListView and provides better performance, more flexibility, and built-in animations.
1. What is RecyclerView?
RecyclerView is an advanced version of ListView that displays a scrollable list of items. It's more flexible and efficient because it:
- Reuses views — Only creates enough views to fill the screen and recycles them as you scroll.
- Supports multiple layouts — You can have different item types in the same list.
- Handles animations — Built-in support for adding, removing, and reordering items.
- Separates concerns — Uses the Adapter pattern to separate data from UI.
RecyclerView Architecture
│ RecyclerView │
│ (Displays the list on screen) │
└─────────────────────────────────────────────────────────────┘
│
┌────────────────┴────────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Adapter │ │ LayoutManager │
│ (Manages data & │ │ (Arranges items on │
│ creates views) │ │ the screen) │
└──────────────────────┘ └──────────────────────┘
│ │
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ ViewHolder │ │ ItemAnimator │
│ (Holds the view for │ │ (Handles add/remove/ │
│ a single item) │ │ reorder animations) │
└──────────────────────┘ └──────────────────────┘
| Component | Responsibility |
|---|---|
| RecyclerView | The UI component that displays the list |
| Adapter | Binds data to views and creates ViewHolders |
| ViewHolder | Holds references to views for a single item |
| LayoutManager | Arranges items (Linear, Grid, Staggered Grid) |
| ItemAnimator | Animates add/remove/reorder changes |
2. Setting Up RecyclerView
Step 1: Add Dependency
Add the following dependency to your build.gradle (Module) file:
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'
}
Step 2: Add RecyclerView to Your Layout
<!-- activity_main.xml -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
3. Creating the Item Layout
Create an XML layout file for each item in the list. For a simple contact list:
<!-- item_contact.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?android:attr/selectableItemBackground">
<!-- Avatar/Initials Circle -->
<TextView
android:id="@+id/avatarText"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/avatar_circle"
android:gravity="center"
android:text="JD"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<!-- Contact Name -->
<TextView
android:id="@+id/nameTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="John Doe"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#333333"
app:layout_constraintStart_toEndOf="@id/avatarText"
app:layout_constraintEnd_toStartOf="@id/statusIcon"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="12dp"
android:layout_marginEnd="8dp" />
<!-- Contact Email / Last Message -->
<TextView
android:id="@+id/emailTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="john@example.com"
android:textSize="14sp"
android:textColor="#666666"
app:layout_constraintStart_toEndOf="@id/avatarText"
app:layout_constraintEnd_toStartOf="@id/statusIcon"
app:layout_constraintTop_toBottomOf="@id/nameTextView"
android:layout_marginStart="12dp" />
<!-- Status Icon (Online/Offline) -->
<View
android:id="@+id/statusIcon"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/status_indicator_green"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
4. Creating the Data Model
// Contact.kt
data class Contact(
val id: Long,
val name: String,
val email: String,
val isOnline: Boolean = false,
val initials: String
) {
constructor(name: String, email: String, isOnline: Boolean = false) : this(
id = System.currentTimeMillis() + name.hashCode(),
name = name,
email = email,
isOnline = isOnline,
initials = name.split(" ")
.mapNotNull { it.firstOrNull()?.toString() }
.joinToString("")
.take(2)
.uppercase()
)
}
5. Creating the Adapter
The Adapter is the core component that binds data to the RecyclerView. It creates ViewHolders and binds data to them.
// ContactAdapter.kt
package com.example.myapp
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
class ContactAdapter(
private var contacts: List<Contact>,
private val onItemClick: (Contact) -> Unit
) : RecyclerView.Adapter<ContactAdapter.ContactViewHolder>() {
// ViewHolder class — holds references to views
class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val avatarText: TextView = itemView.findViewById(R.id.avatarText)
val nameTextView: TextView = itemView.findViewById(R.id.nameTextView)
val emailTextView: TextView = itemView.findViewById(R.id.emailTextView)
val statusIcon: View = itemView.findViewById(R.id.statusIcon)
}
// 1. Create new ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_contact, parent, false)
return ContactViewHolder(view)
}
// 2. Bind data to ViewHolder
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
val contact = contacts[position]
// Set avatar initials
holder.avatarText.text = contact.initials
// Set name
holder.nameTextView.text = contact.name
// Set email
holder.emailTextView.text = contact.email
// Set status indicator color
val statusColor = if (contact.isOnline) {
ContextCompat.getColor(holder.itemView.context, R.color.status_online)
} else {
ContextCompat.getColor(holder.itemView.context, R.color.status_offline)
}
holder.statusIcon.setBackgroundColor(statusColor)
// Set click listener
holder.itemView.setOnClickListener {
onItemClick(contact)
}
}
// 3. Return number of items
override fun getItemCount(): Int = contacts.size
// 4. Update data and refresh the list
fun updateData(newContacts: List<Contact>) {
contacts = newContacts
notifyDataSetChanged()
}
}
6. Setting Up the Activity
// MainActivity.kt
package com.example.myapp
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ContactAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. Initialize RecyclerView
recyclerView = findViewById(R.id.recyclerView)
// 2. Set LayoutManager
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
// 3. Create sample data
val contacts = listOf(
Contact("Alice Johnson", "alice@example.com", true),
Contact("Bob Smith", "bob@example.com", false),
Contact("Charlie Brown", "charlie@example.com", true),
Contact("Diana Prince", "diana@example.com", false),
Contact("Eve Wilson", "eve@example.com", true),
Contact("Frank Castle", "frank@example.com", false),
)
// 4. Create and set adapter
adapter = ContactAdapter(contacts) { contact ->
Toast.makeText(this, "Selected: ${contact.name}", Toast.LENGTH_SHORT).show()
}
recyclerView.adapter = adapter
}
}
7. LayoutManager Types
RecyclerView supports multiple layout managers for different visual arrangements:
| LayoutManager | Description | Use Case |
|---|---|---|
| LinearLayoutManager | Arranges items in a single column or row | Standard lists (vertical/horizontal) |
| GridLayoutManager | Arranges items in a grid | Photo galleries, product grids |
| StaggeredGridLayoutManager | Arranges items in a staggered grid | Pinterest-style layouts |
// Vertical Linear Layout (default)
val linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager
// Horizontal Linear Layout
val horizontalLayoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerView.layoutManager = horizontalLayoutManager
// Grid Layout (2 columns)
val gridLayoutManager = GridLayoutManager(this, 2)
recyclerView.layoutManager = gridLayoutManager
// Staggered Grid Layout (2 columns)
val staggeredGridLayoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
recyclerView.layoutManager = staggeredGridLayoutManager
8. Adding Item Click Handling
There are multiple ways to handle item clicks in RecyclerView. Here's the recommended approach using a click listener interface:
// 1. Define click listener interface
interface OnItemClickListener {
fun onItemClick(position: Int)
fun onItemLongClick(position: Int): Boolean
}
// 2. Modify adapter to include click listener
class ContactAdapter(
private var contacts: List<Contact>,
private val clickListener: OnItemClickListener
) : RecyclerView.Adapter<ContactAdapter.ContactViewHolder>() {
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
// ... binding code ...
holder.itemView.setOnClickListener {
clickListener.onItemClick(position)
}
holder.itemView.setOnLongClickListener {
clickListener.onItemLongClick(position)
true
}
}
}
// 3. Implement in Activity
class MainActivity : AppCompatActivity(), OnItemClickListener {
override fun onItemClick(position: Int) {
val contact = contacts[position]
Toast.makeText(this, "Clicked: ${contact.name}", Toast.LENGTH_SHORT).show()
}
override fun onItemLongClick(position: Int): Boolean {
val contact = contacts[position]
Toast.makeText(this, "Long pressed: ${contact.name}", Toast.LENGTH_SHORT).show()
return true
}
}
9. DiffUtil — Efficient Updates
DiffUtil calculates the difference between two lists and updates only the changed items, improving performance for large lists.
// ContactDiffCallback.kt
class ContactDiffCallback(
private val oldList: List<Contact>,
private val newList: List<Contact>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
// In Adapter — update using DiffUtil
fun updateData(newContacts: List<Contact>) {
val diffResult = DiffUtil.calculateDiff(
ContactDiffCallback(contacts, newContacts)
)
contacts = newContacts
diffResult.dispatchUpdatesTo(this)
}
10. Multiple View Types
You can display different item layouts in the same RecyclerView:
// In Adapter
companion object {
private const val VIEW_TYPE_CONTACT = 0
private const val VIEW_TYPE_HEADER = 1
private const val VIEW_TYPE_FOOTER = 2
}
override fun getItemViewType(position: Int): Int {
return when {
position == 0 -> VIEW_TYPE_HEADER
position == itemCount - 1 -> VIEW_TYPE_FOOTER
else -> VIEW_TYPE_CONTACT
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_HEADER -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_header, parent, false)
HeaderViewHolder(view)
}
VIEW_TYPE_FOOTER -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_footer, parent, false)
FooterViewHolder(view)
}
else -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_contact, parent, false)
ContactViewHolder(view)
}
}
}
11. Common Pitfalls & Solutions
Pitfall 1: Not using ViewHolder pattern correctly
Problem: Finding views in onBindViewHolder every time causes performance issues.
Solution: Always find and store view references in the ViewHolder constructor, not in onBindViewHolder.
Pitfall 2: Not using the correct LayoutManager for your use case
Problem: Using LinearLayoutManager when GridLayoutManager would be more appropriate.
Solution: Choose the right LayoutManager for your data presentation needs.
Pitfall 3: Creating new objects in onBindViewHolder
Problem: Creating objects (like listeners) every time a view is bound creates memory churn.
Solution: Use a single listener instance or set listeners in the ViewHolder constructor.
Pitfall 4: Using notifyDataSetChanged() for small updates
Problem: notifyDataSetChanged() redraws the entire list, which is inefficient for small changes.
Solution: Use notifyItemInserted(), notifyItemRemoved(), notifyItemChanged(), or DiffUtil.
12. Production-Ready Checklist
- ✅ Use ViewHolder pattern — Hold view references for performance.
- ✅ Use DiffUtil — For efficient list updates.
- ✅ Choose the right LayoutManager — Linear, Grid, or Staggered Grid.
- ✅ Handle item clicks — Use an interface for clean separation.
- ✅ Support multiple view types — If needed, implement
getItemViewType(). - ✅ Use stable IDs — Override
getItemId()andsetHasStableIds(true). - ✅ Set LayoutManager efficiently — Initialize once, not on every configuration change.
- ✅ Add scroll listeners — For infinite scrolling/pagination.
- ✅ Use item decorations — For dividers and spacing.
- ✅ Test with large datasets — Ensure smooth scrolling with 1000+ items.
- ✅ Handle configuration changes — Save and restore scroll position.
- ✅ Add animations — Use
RecyclerView.ItemAnimatorfor add/remove animations.
13. Quick Reference — RecyclerView Methods
| Method | Description |
|---|---|
onCreateViewHolder() | Called to create a new ViewHolder |
onBindViewHolder() | Called to bind data to a ViewHolder |
getItemCount() | Returns the number of items |
getItemViewType() | Returns the view type for a position |
notifyDataSetChanged() | Refresh all items |
notifyItemInserted() | Refresh a single inserted item |
notifyItemRemoved() | Refresh a single removed item |
notifyItemChanged() | Refresh a single changed item |
notifyItemRangeInserted() | Refresh a range of inserted items |
Ready to master Android Development?
Build real-world Android apps with hands-on projects, mentor-led sessions, and placement support.
.png)