RecyclerView Basics

RecipeBox's main screen is a scrolling list of recipes — potentially hundreds of them once a user has been cooking and saving for a while. RecyclerView is the standard Android widget for exactly this: displaying a long, scrolling collection of items efficiently.

Why "Recycler"? The Problem It Solves

A naive list would create a brand-new row view for every single item — 300 recipes means 300 inflated layouts sitting in memory, most of which are scrolled off-screen and invisible at any given moment. RecyclerView instead creates only enough row views to fill the visible screen (plus a small buffer), and as the user scrolls, it reuses those same view objects — just swapping in new data — rather than inflating new ones. This is why it stays smooth whether RecipeBox has 5 recipes or 5,000.

Scroll down

Row that left the top of the screen

→

Recycled

Its View object is reused, not discarded

→

Rebound

New recipe's data is bound into it

→

Appears

At the bottom of the screen

The Three Pieces You Need

Building any RecyclerView list comes down to three parts working together:

  1. A layout for one item — what a single row looks like.
  2. An Adapter — connects your data (a list of Recipe objects) to those item views.
  3. A LayoutManager — decides how items are arranged (a vertical list, a grid, etc.).

1. The Item Layout: item_recipe.xml

Each row shows a thumbnail photo, the recipe title, and its prep time:

<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="2dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="12dp">

        <ImageView
            android:id="@+id/ivThumbnail"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:scaleType="centerCrop"
            android:contentDescription="Recipe photo" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="12dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tvTitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/tvPrepMinutes"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />

        </LinearLayout>
    </LinearLayout>
</androidx.cardview.widget.CardView>

2 & 3. The Adapter and ViewHolder

The ViewHolder is a small static class that just holds findViewById() references for one row, so they're looked up once instead of on every scroll. The Adapter creates ViewHolders, binds data into them, and reports how many items exist:

// RecipeAdapter.java
public class RecipeAdapter extends RecyclerView.Adapter<RecipeAdapter.RecipeViewHolder> {

    public interface OnRecipeClickListener {
        void onRecipeClick(Recipe recipe);
    }

    private final List<Recipe> recipes;
    private final OnRecipeClickListener clickListener;

    public RecipeAdapter(List<Recipe> recipes, OnRecipeClickListener clickListener) {
        this.recipes = recipes;
        this.clickListener = clickListener;
    }

    @Override
    public RecipeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_recipe, parent, false);
        return new RecipeViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(RecipeViewHolder holder, int position) {
        Recipe recipe = recipes.get(position);
        holder.tvTitle.setText(recipe.title);
        holder.tvPrepMinutes.setText(recipe.prepMinutes + " min");
        holder.ivThumbnail.setImageURI(Uri.parse(recipe.photoUri));

        holder.itemView.setOnClickListener(v -> clickListener.onRecipeClick(recipe));
    }

    @Override
    public int getItemCount() {
        return recipes.size();
    }

    static class RecipeViewHolder extends RecyclerView.ViewHolder {
        ImageView ivThumbnail;
        TextView tvTitle;
        TextView tvPrepMinutes;

        RecipeViewHolder(View itemView) {
            super(itemView);
            ivThumbnail = itemView.findViewById(R.id.ivThumbnail);
            tvTitle = itemView.findViewById(R.id.tvTitle);
            tvPrepMinutes = itemView.findViewById(R.id.tvPrepMinutes);
        }
    }
}
Explanation:
  • onCreateViewHolder() is called only as often as needed to fill the screen (plus a small buffer) — it inflates item_recipe.xml and wraps it in a fresh RecipeViewHolder.
  • onBindViewHolder() is called far more often, every time a recycled row needs new data — it must stay cheap, since it runs on every scroll.
  • getItemCount() tells RecyclerView how many rows exist in total, so it knows how far the scrollbar and recycling logic should go.

Handling Item Clicks

Java has no equivalent of Kotlin's trailing-lambda syntax for passing a click handler straight into a constructor call, so the cleanest pattern is a small callback interface (OnRecipeClickListener above) implemented by whoever creates the adapter — usually the hosting Activity or Fragment:

// As an anonymous class
RecipeAdapter adapter = new RecipeAdapter(recipes, new RecipeAdapter.OnRecipeClickListener() {
    @Override
    public void onRecipeClick(Recipe recipe) {
        openRecipeDetail(recipe.id);
    }
});

// Or, since the interface has exactly one method, as a lambda
RecipeAdapter adapter = new RecipeAdapter(recipes, recipe -> openRecipeDetail(recipe.id));

Wiring It Together in the Activity

RecyclerView recyclerView = findViewById(R.id.rvRecipes);

List<Recipe> recipes = recipeRepository.getAllRecipes();
RecipeAdapter adapter = new RecipeAdapter(recipes, recipe -> openRecipeDetail(recipe.id));

recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
Explanation: LinearLayoutManager arranges items in a single vertical (or horizontal) line — the right choice for RecipeBox's recipe list. GridLayoutManager is the alternative when you want a photo-grid style layout instead, arranging items into a fixed number of columns.
Next up: what happens when the recipe list changes and you don't want to redraw every visible row → RecyclerView: DiffUtil & Pagination.