Back to Course
Lists

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.

Production Reality: RecyclerView is the most frequently used UI component in modern Android apps. Mastering it is essential for building professional applications that handle large datasets efficiently.

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 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) │
└──────────────────────┘ └──────────────────────┘
ComponentResponsibility
RecyclerViewThe UI component that displays the list
AdapterBinds data to views and creates ViewHolders
ViewHolderHolds references to views for a single item
LayoutManagerArranges items (Linear, Grid, Staggered Grid)
ItemAnimatorAnimates 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:

LayoutManagerDescriptionUse Case
LinearLayoutManagerArranges items in a single column or rowStandard lists (vertical/horizontal)
GridLayoutManagerArranges items in a gridPhoto galleries, product grids
StaggeredGridLayoutManagerArranges items in a staggered gridPinterest-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

Common Mistakes to Avoid:

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() and setHasStableIds(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.ItemAnimator for add/remove animations.

13. Quick Reference — RecyclerView Methods

MethodDescription
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
Key Takeaway: RecyclerView is the backbone of data presentation in Android. Understanding the Adapter, ViewHolder, and LayoutManager pattern is essential for building any app that displays lists. Use DiffUtil for efficient updates and always test with large datasets to ensure smooth performance.

Ready to master Android Development?

Build real-world Android apps with hands-on projects, mentor-led sessions, and placement support.

Explore Course