Android UI, Activities & Navigation

RecyclerView & Lists

18 min Lesson 4 of 12

RecyclerView & Lists

Every real Android application displays lists: a feed of messages, a catalogue of products, a history of transactions. The widget designed for this job is RecyclerView. It replaced the older ListView because it solves two hard problems at once: it recycles the view objects that scroll off screen rather than destroying and recreating them, and it imposes no constraints on how items are arranged or animated. Understanding how RecyclerView works — and why it is structured the way it is — will make you a more effective Android developer across every feature you build.

Why RecyclerView Exists

Imagine displaying 10,000 contacts. Inflating 10,000 View objects at once would consume hundreds of megabytes of memory and freeze the UI thread. RecyclerView solves this by maintaining a small pool of view objects — typically only slightly more than the number visible on screen — and recycling them as the user scrolls. When a row scrolls off the top, its view is detached from the window and placed back in the pool. When a new row needs to appear at the bottom, a pooled view is retrieved, its contents are overwritten with the new data, and it is attached to the window.

The key insight: RecyclerView never inflates more view objects than it needs to fill the screen. Your data set can have a million items; the memory footprint for the list UI stays constant.

The Three-Part Architecture

RecyclerView is intentionally split into three separate collaborating objects, each with a single responsibility:

  • RecyclerView — the ViewGroup that goes in your layout XML. It manages the item pool, handles touch events, and orchestrates scrolling.
  • LayoutManager — decides where to position items. LinearLayoutManager gives you a vertical or horizontal list; GridLayoutManager gives you a grid; StaggeredGridLayoutManager gives you Pinterest-style unequal rows.
  • Adapter — bridges your data and the view pool. It inflates new view holders when the pool is empty, and binds fresh data into a recycled view holder when one is needed.

Adding RecyclerView to Your Layout

First, add the dependency to your build.gradle (app module). In modern projects it usually arrives via the appcompat or material dependency, but you can also declare it directly:

// build.gradle (app) dependencies { implementation 'androidx.recyclerview:recyclerview:1.3.2' }

Then place it in your activity layout:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:padding="8dp" /> </LinearLayout>

Defining the Item Layout

Each row needs its own layout file. Create res/layout/item_contact.xml:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="12dp"> <TextView android:id="@+id/tvName" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/tvPhone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="#666666" /> </LinearLayout>

The Model Class

A simple plain Java object to hold the data for one row:

public class Contact { private final String name; private final String phone; public Contact(String name, String phone) { this.name = name; this.phone = phone; } public String getName() { return name; } public String getPhone() { return phone; } }

Building the Adapter and ViewHolder

This is where the real work happens. The Adapter has three responsibilities, expressed as three methods you must override:

  • onCreateViewHolder — inflate the item layout and wrap it in a ViewHolder. Called only when the pool has no recycled holder available.
  • onBindViewHolder — take a recycled (or freshly created) holder and populate it with the data at a given position. Called every time a row is about to appear on screen.
  • getItemCount — tell the RecyclerView how many items exist.

The ViewHolder is an inner class that caches the View references for one row. Without it, every call to onBindViewHolder would have to call findViewById on the root view — an expensive tree traversal — on every single scroll event.

import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> { private final List<Contact> contacts; public ContactAdapter(List<Contact> contacts) { this.contacts = contacts; } // 1. Inflate the row layout and create a ViewHolder @NonNull @Override public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_contact, parent, false); return new ContactViewHolder(itemView); } // 2. Bind data to the recycled ViewHolder @Override public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) { Contact contact = contacts.get(position); holder.tvName.setText(contact.getName()); holder.tvPhone.setText(contact.getPhone()); } // 3. Report the total item count @Override public int getItemCount() { return contacts.size(); } // ViewHolder caches the child view references for one row static class ContactViewHolder extends RecyclerView.ViewHolder { final TextView tvName; final TextView tvPhone; ContactViewHolder(@NonNull View itemView) { super(itemView); tvName = itemView.findViewById(R.id.tvName); tvPhone = itemView.findViewById(R.id.tvPhone); } } }
Keep onBindViewHolder fast. It runs on the UI thread and is called for every row that enters the viewport during scrolling. No network calls, no disk I/O, no complex computation here — only data reads and setText / setImageBitmap style calls. Anything heavier belongs in a background thread before this point.

Wiring Everything Together in the Activity

import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; public class ContactListActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contact_list); // Build sample data List<Contact> contacts = new ArrayList<>(); contacts.add(new Contact("Alice Martin", "+1 555-0101")); contacts.add(new Contact("Bob Chen", "+1 555-0202")); contacts.add(new Contact("Sara Ali", "+1 555-0303")); // Wire up the RecyclerView RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new ContactAdapter(contacts)); } }

Three lines do all the work: set a LayoutManager (vertical list by default), create the adapter with the data, and assign it. RecyclerView will immediately ask the adapter how many items exist and start inflating and binding as needed.

Updating the List Efficiently

Calling notifyDataSetChanged() forces a full rebind of every visible row — it works, but it skips item-change animations and is wasteful. Prefer the targeted notification methods when you know exactly what changed:

// A row was inserted at position 2 adapter.notifyItemInserted(2); // The item at position 5 was updated adapter.notifyItemChanged(5); // Items at positions 1 through 3 were removed adapter.notifyItemRangeRemoved(1, 3);
Never modify the data list from a background thread and then call notify methods on the main thread without synchronisation. The adapter reads the list in onBindViewHolder on the UI thread. A concurrent write from another thread will cause a crash or produce stale visuals. Always perform list mutations on the main thread, or use DiffUtil (covered in the next lesson) which hands the final swap back to the main thread safely.

Handling Item Clicks

Unlike ListView, RecyclerView has no built-in click listener. The idiomatic pattern is to pass a callback interface into the adapter:

public interface OnContactClickListener { void onContactClick(Contact contact); } // In ContactAdapter constructor: public ContactAdapter(List<Contact> contacts, OnContactClickListener listener) { this.contacts = contacts; this.listener = listener; } // In onBindViewHolder: holder.itemView.setOnClickListener(v -> listener.onContactClick(contacts.get(holder.getAdapterPosition())));

Pass a lambda from the activity: new ContactAdapter(contacts, contact -> openDetail(contact)). This keeps the adapter free of any knowledge of navigation or business logic.

Summary

RecyclerView is built on three objects working together: the widget itself, a LayoutManager that places items, and an Adapter that inflates and binds them. The ViewHolder pattern is mandatory — it caches findViewByid lookups so that onBindViewHolder stays fast during scrolling. Structuring click handling as a callback interface keeps the adapter focused and testable. In the next lesson you will replace the static list with live data fetched from a database or network, and learn how DiffUtil computes minimal updates automatically.