Android UI, Activities & Navigation

Binding Data to Lists

18 min Lesson 5 of 12

Binding Data to Lists

In the previous lesson you built the structural scaffolding: a RecyclerView in your layout, a ViewHolder class, and a bare-bones RecyclerView.Adapter. Now the real work begins — supplying a live data set, wiring it to the adapter, and understanding exactly where Android calls your code and why. This lesson focuses entirely on the adapter contract and the patterns a professional Android developer applies to keep UI and data in sync.

Recap: What the Adapter Actually Does

Think of a RecyclerView.Adapter as the bridge between a plain Java list and the rows you see on screen. It answers three questions that the RecyclerView asks repeatedly:

  1. How many items are there?getItemCount()
  2. Create a view holder for this view type.onCreateViewHolder()
  3. Bind the data at position N into this holder.onBindViewHolder()

All the data-binding logic lives in onBindViewHolder(). Everything else is plumbing.

A Concrete Model Class

Good adapters operate on typed model objects, not raw strings or maps. Define a plain Java model for the items in your list:

// Product.java public class Product { private final String name; private final String category; private final double price; public Product(String name, String category, double price) { this.name = name; this.category = category; this.price = price; } public String getName() { return name; } public String getCategory() { return category; } public double getPrice() { return price; } }

Immutable fields with only getters are a safe default for list items — they cannot be accidentally mutated while the adapter is reading them.

A Complete ProductAdapter

Here is a full adapter for a RecyclerView that shows a list of Product objects. Read the inline comments carefully — each line corresponds to a platform rule.

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.ArrayList; import java.util.List; import java.util.Locale; public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> { // The adapter owns a COPY of the data, not a reference to the caller's list. private final List<Product> items; public ProductAdapter(List<Product> products) { // Defensive copy: insulates the adapter from external mutations. this.items = new ArrayList<>(products); } // --- Inflation --- @NonNull @Override public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // LayoutInflater must use the parent's context and must NOT attach immediately. View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_product, parent, false); return new ProductViewHolder(itemView); } // --- Binding --- @Override public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) { Product product = items.get(position); holder.nameView.setText(product.getName()); holder.categoryView.setText(product.getCategory()); // Format price with two decimal places and a currency symbol. holder.priceView.setText(String.format(Locale.getDefault(), "$%.2f", product.getPrice())); } // --- Count --- @Override public int getItemCount() { return items.size(); } // --- ViewHolder --- static class ProductViewHolder extends RecyclerView.ViewHolder { final TextView nameView; final TextView categoryView; final TextView priceView; ProductViewHolder(@NonNull View itemView) { super(itemView); // findViewById is called ONCE here, not on every bind. nameView = itemView.findViewById(R.id.tv_product_name); categoryView = itemView.findViewById(R.id.tv_product_category); priceView = itemView.findViewById(R.id.tv_product_price); } } }
Why inflate with attachToRoot = false? Passing false as the third argument to inflate() tells the inflater to use the parent only for layout parameter context, not to attach the view immediately. RecyclerView manages attachment itself. If you pass true, the view gets attached twice and you see a crash or garbled layout at runtime.

The Corresponding Item Layout

The layout file res/layout/item_product.xml inflated above might look like this:

<?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="vertical" android:padding="12dp"> <TextView android:id="@+id/tv_product_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/tv_product_category" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" /> <TextView android:id="@+id/tv_product_price" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" /> </LinearLayout>

Wiring the Adapter to the RecyclerView in Your Activity

In your Activity (or Fragment), create the data, instantiate the adapter, and attach it once:

// ProductListActivity.java import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.os.Bundle; import java.util.Arrays; import java.util.List; public class ProductListActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_product_list); List<Product> products = Arrays.asList( new Product("Laptop Pro", "Electronics", 1299.99), new Product("Desk Chair", "Furniture", 349.00), new Product("Notebook", "Stationery", 4.50), new Product("Headphones", "Electronics", 199.99) ); RecyclerView recyclerView = findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new ProductAdapter(products)); } }

Updating the List: notifyDataSetChanged vs. Targeted Notifications

When your data changes you must tell the adapter. The lazy approach — adapter.notifyDataSetChanged() — works but redraws every visible row even if only one item changed. Android provides surgical alternatives:

  • notifyItemInserted(int position) — triggers the insert animation for a single row.
  • notifyItemRemoved(int position) — triggers the remove animation.
  • notifyItemChanged(int position) — redraws one row without animation.
  • notifyItemRangeInserted(int positionStart, int itemCount) — efficient bulk insert.

A typical pattern is to expose a method on the adapter that mutates its internal list and fires the correct notification:

// Inside ProductAdapter — add to the class public void addProduct(Product product) { items.add(product); notifyItemInserted(items.size() - 1); } public void removeProduct(int position) { items.remove(position); notifyItemRemoved(position); } public void replaceAll(List<Product> newProducts) { items.clear(); items.addAll(newProducts); notifyDataSetChanged(); // full replacement — targeted notify not practical here }
Prefer targeted notifications for smooth animations. notifyDataSetChanged() forces a full redraw and suppresses the default item change animations. Use it only when the entire data set is swapped out. For incremental changes (add one row, delete one row) the targeted methods look far better to the user and are also more efficient.

The Stable IDs Optimisation

If every item in your data set has a unique, persistent identifier (a database primary key, for example), enable stable IDs. This lets RecyclerView track items across data set changes and apply the correct animations even after notifyDataSetChanged():

public ProductAdapter(List<Product> products) { this.items = new ArrayList<>(products); setHasStableIds(true); // call in constructor } @Override public long getItemId(int position) { return items.get(position).getId(); // return the unique DB id }
Do not call setHasStableIds(true) without overriding getItemId(). The default implementation returns RecyclerView.NO_ID (-1) for every item, so RecyclerView cannot distinguish between rows, and animations will be incorrect or crash-prone. If you opt into stable IDs, you must provide a genuinely unique value from your model.

What About DiffUtil?

DiffUtil is the production-grade solution for computing the minimal set of changes between two lists and dispatching the exact targeted notifications automatically. It is built on the Myers diff algorithm and runs on a background thread. You will encounter it in real codebases, but it builds directly on the notification APIs you have just learned — master those first. A future lesson in this course covers DiffUtil and ListAdapter in depth.

Summary

Feeding data into a RecyclerView comes down to three moving parts: a typed model class, an adapter that implements the three required callbacks, and a layout manager that tells RecyclerView how to arrange the rows. The binding logic all lives in onBindViewHolder(); keep it fast — no database calls, no heavy computation. When data changes, fire the most targeted notification possible. With these fundamentals in place you are ready to add item click handling and richer row layouts in the lessons ahead.