A RecyclerView is a modern, flexible, and efficient version of ListView. It is the standard for displaying lists in Android today.
While ListView only shows items vertically, RecyclerView can show items in:
- Vertical Lists
- Horizontal Lists
- Grids (like Instagram Profile)
- Staggered Grids (like Pinterest)
ListView was the old standard, but it had major flaws that RecyclerView fixes.
| Feature | ListView | RecyclerView |
|---|---|---|
| Performance | Low. Used findViewById repeatedly unless you manually implemented ViewHolder pattern. |
High. Forces you to use the ViewHolder pattern, so views are cached automatically. |
| Layouts | Only Vertical lists. | Flexible. Vertical, Horizontal, Grid, Staggered Grid. |
| Animations | Very hard to implement (add/remove items). | Built-in support for item animations. |
| Click Listeners | Built-in onItemClick. |
No built-in listener. You must implement your own (gives more control). |
| Decoration | Simple dividers only. | ItemDecoration class allows complex dividers, spacing, and drawing. |
While both use notifyDataSetChanged(), RecyclerView is much smarter.
| Feature | ListView | RecyclerView |
|---|---|---|
| Refresh All | adapter.notifyDataSetChanged() (Redraws the whole screen) |
adapter.notifyDataSetChanged() (Redraws the whole screen) |
| Add Single Item | Not available. | adapter.notifyItemInserted(position) |
| Remove Single Item | Not available. | adapter.notifyItemRemoved(position) |
| Update Single Item | Not available. | adapter.notifyItemChanged(position) |
When moving from ListView to RecyclerView, the files change slightly.
- LayoutManager (New Concept): In ListView, verticality was hardcoded. In RecyclerView, you must tell it how to arrange items (Linear, Grid, etc.).
- Adapter Changes: Instead of extending
ArrayAdapter, we extendRecyclerView.Adapter. - ViewHolder (Mandatory): It is no longer an optional optimization; it is a required inner class.
| Layout Style | Constructor & Parameters | Visual Description | Real World Example |
|---|---|---|---|
| Linear Vertical (Default) | new LinearLayoutManager(context)Params: Just the Context ( this or requireContext()). |
Items stacked Top to Bottom. Standard list behavior. | • WhatsApp Chat List • Gmail Inbox • Contacts App |
| Linear Horizontal | new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)Params: 1. Context 2. HORIZONTAL: Scroll direction.3. reverse: false (Standard LTR), true (Start from right). |
Items arranged Left to Right. A "Carousel" or side-scrolling strip. | • Instagram Stories bar • Netflix "Trending Now" row • Weather App hourly forecast |
| Linear (Reverse) | new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)Params: reverse: true |
Items stacked Bottom to Top. When you add a new item, it appears at the bottom, and you start scrolled to the bottom. | • Inside a Chat Message (Messenger/WhatsApp) where newest text is at the bottom. |
| Grid Vertical | new GridLayoutManager(context, 2)Params: 1. Context 2. spanCount: 2 (Number of Columns). |
Items arranged in a grid with Fixed Columns. All items have the same height/width box. | • Instagram Profile (3 columns) • Photo Gallery • File Explorer |
| Grid Horizontal | new GridLayoutManager(context, 2, GridLayoutManager.HORIZONTAL, false)Params: spanCount: 2 (Becomes number of Rows now). |
Items fill column 1 (top-down), then move right to column 2. Scroll is sideways. | • Emoji Keyboard (paged horizontally) • TV Streaming App "All Movies" list |
| Staggered Grid | new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)Params: 1. spanCount: 2 (Columns).2. Orientation. (Note: No Context needed). |
Masonry Layout. Items have different heights, so they fit together like bricks without gaps. | • Pinterest Feed • Google Keep Notes • Wallpapers App |
StaggeredGridLayoutManager:
- Unlike the normal Grid, this does not force squares.
- If Image A is tall and Image B is short, Image C will slide up to fill the empty space below Image B. It looks organic and messy (in a good way).
If using RecyclerView inside a Fragment:
- Use
view.findViewByIdinonViewCreated. - Pass
requireContext()orgetContext()to the LayoutManager.
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
RecyclerView rv = view.findViewById(R.id.rv_fragment);
rv.setLayoutManager(new LinearLayoutManager(requireContext())); // Context needed here
rv.setAdapter(myAdapter);
}AppData: Stores the global list of contacts.Person: The simple object class of one person.PersonGridAdapter: Handles the grid and clicks for the contact list.PersonListFragment: Shows the grid, handles Zoom & Delete logic.PersonDetailFragment: Shows full details of person.MainActivity: Handles navigation.
Person.java (The Model)
Simple class to hold data.
public class Person {
String name;
String details;
int imageResId; // drawable ID (e.g., R.drawable.ic_face)
int color; // Background color (e.g., Color.BLUE)
public Person(String name, String details, int imageResId, int color) {
this.name = name;
this.details = details;
this.imageResId = imageResId;
this.color = color;
}
}AppData.java (Global Storage)
Don't forget to add android:name=".AppData" in your Manifest!
import android.app.Application;
import android.graphics.Color;
import java.util.ArrayList;
import java.util.List;
public class AppData extends Application {
public static List<Person> personList = new ArrayList<>();
@Override
public void onCreate() {
super.onCreate();
// Add dummy data
personList.add(new Person("Ali", "Software Engineer", R.drawable.ic_launcher_foreground, Color.parseColor("#FFCDD2"))); // Red
personList.add(new Person("Sara", "Doctor", R.drawable.ic_launcher_foreground, Color.parseColor("#C8E6C9"))); // Green
personList.add(new Person("John", "Pilot", R.drawable.ic_launcher_foreground, Color.parseColor("#BBDEFB"))); // Blue
personList.add(new Person("Mina", "Artist", R.drawable.ic_launcher_foreground, Color.parseColor("#E1BEE7"))); // Purple
}
}item_person_grid.xml (The Grid Box)
A CardView makes the box look nice. We use CardView to easily make the image circular if needed, or just standard ImageViews.
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardRoot"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_margin="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:id="@+id/layoutContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp">
<androidx.cardview.widget.CardView
android:layout_width="80dp"
android:layout_height="80dp"
app:cardCornerRadius="40dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/imgPerson"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Name"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
fragment_person_list.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
android:clipToPadding="false"/>
</FrameLayout>
This handles:
- Click on Image -> Zoom
- Click on Box -> Details
- Long Click on Box -> Delete Alert
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class PersonGridAdapter extends RecyclerView.Adapter<PersonGridAdapter.MyViewHolder> {
List<Person> list;
OnItemAction listener; // Our custom interface
// Interface to talk to Fragment
public interface OnItemAction {
void onPersonClick(Person p); // Open Details
void onImageClick(Person p); // Zoom Image
void onDeleteRequest(int position); // Delete Alert
}
public PersonGridAdapter(List<Person> list, OnItemAction listener) {
this.list = list;
this.listener = listener;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_person_grid, parent, false);
return new MyViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
Person p = list.get(position);
// 1. Set Data
holder.tvName.setText(p.name);
holder.imgProfile.setImageResource(p.imageResId);
holder.container.setBackgroundColor(p.color);
// 2. Click on the WHOLE BOX -> Open Details
holder.itemView.setOnClickListener(v -> listener.onPersonClick(p));
// 3. Click ONLY on IMAGE -> Zoom
holder.imgProfile.setOnClickListener(v -> listener.onImageClick(p));
// 4. Long Press -> Delete
holder.itemView.setOnLongClickListener(v -> {
listener.onDeleteRequest(position);
return true; // Consumes the click so normal click doesn't fire
});
}
@Override
public int getItemCount() {
return list.size();
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
TextView tvName;
ImageView imgProfile;
LinearLayout container;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tvName);
imgProfile = itemView.findViewById(R.id.imgPerson);
container = itemView.findViewById(R.id.layoutContainer);
}
}
}This class implements the Adapter interface and handles the visual logic (Dialogs, Zooming).
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager; // For Grid
import androidx.recyclerview.widget.RecyclerView;
public class PersonListFragment extends Fragment {
// Interface to talk to MainActivity (Navigation)
public interface FragmentNavigation {
void navigateToDetails(Person p);
}
FragmentNavigation navListener;
PersonGridAdapter adapter;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_person_list, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 1. Connect Navigation Listener (Activity)
if (getActivity() instanceof FragmentNavigation) {
navListener = (FragmentNavigation) getActivity();
}
RecyclerView rv = view.findViewById(R.id.recyclerView);
// 2. Set Grid Layout (2 Columns) -> Left & Right
rv.setLayoutManager(new GridLayoutManager(getContext(), 2));
// 3. Create Adapter with Logic
adapter = new PersonGridAdapter(AppData.personList, new PersonGridAdapter.OnItemAction() {
@Override
public void onPersonClick(Person p) {
// Send info to Activity to change fragment
if (navListener != null) navListener.navigateToDetails(p);
}
@Override
public void onImageClick(Person p) {
// Show Zoomed Image Dialog
showZoomDialog(p);
}
@Override
public void onDeleteRequest(int position) {
// Show Alert Dialog
showDeleteAlert(position);
}
});
rv.setAdapter(adapter);
}
// --- LOGIC: DELETE ---
private void showDeleteAlert(int position) {
new AlertDialog.Builder(requireContext())
.setTitle("Delete?")
.setMessage("Are you sure you want to remove this person?")
.setPositiveButton("Yes", (dialog, which) -> {
// 1. Remove from Data Source
AppData.personList.remove(position);
// 2. Notify Adapter (Efficient animation)
adapter.notifyItemRemoved(position);
adapter.notifyItemRangeChanged(position, AppData.personList.size());
})
.setNegativeButton("No", null)
.show();
}
// --- LOGIC: ZOOM IMAGE ---
private void showZoomDialog(Person p) {
// Create a Dialog
Dialog dialog = new Dialog(requireContext());
// Create views programmatically (Dynamic)
CardView card = new CardView(requireContext());
card.setRadius(300f); // Make it very circular
card.setCardElevation(10f);
ImageView img = new ImageView(requireContext());
img.setImageResource(p.imageResId);
img.setScaleType(ImageView.ScaleType.CENTER_CROP);
// Add Image to Card
card.addView(img);
// Set Dialog properties
dialog.setContentView(card);
// Set Dynamic Size (e.g., 800x800)
dialog.getWindow().setLayout(800, 800);
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show();
}
}MainActivity.java
Implements the navigation interface to switch fragments.
public class MainActivity extends AppCompatActivity implements PersonListFragment.FragmentNavigation {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Load List Fragment initially
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new PersonListFragment())
.commit();
}
}
@Override
public void navigateToDetails(Person p) {
// 1. Create Detail Fragment
PersonDetailFragment detailFrag = new PersonDetailFragment();
// 2. Pass Data via Bundle
Bundle args = new Bundle();
args.putString("NAME", p.name);
args.putString("INFO", p.details);
detailFrag.setArguments(args);
// 3. Switch Fragment
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, detailFrag)
.addToBackStack(null) // Allows going back
.commit();
}
}PersonDetailFragment.java
Receives the bundle and shows info.
public class PersonDetailFragment extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate simple layout with 2 TextViews
return inflater.inflate(R.layout.fragment_person_detail, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
TextView tvName = view.findViewById(R.id.tvDetailName);
TextView tvInfo = view.findViewById(R.id.tvDetailInfo);
if (getArguments() != null) {
tvName.setText(getArguments().getString("NAME"));
tvInfo.setText(getArguments().getString("INFO"));
}
}
}Standard Definition: This parameter allows a single RecyclerView to show different types of layouts in the same list.
- If your list looks the same everywhere (e.g., a simple contact list):
You can ignore this parameter. By default, it is always
0. - When you actually use it: Think of a Chat App.
- Right-side bubble: (My messages)
viewType = 1 - Left-side bubble: (Friend's messages)
viewType = 2
Inside onCreateViewHolder, you would use it like this:
if (viewType == 1) {
// Inflate my_message.xml
} else {
// Inflate friend_message.xml
}It represents the Root View of your row's XML file.
When you wrote your XML for the grid item (item_person_grid.xml), it looked like this:
<androidx.cardview.widget.CardView xmlns:android="..."
android:id="@+id/cardRoot" ... >
<LinearLayout ...>
<ImageView ... />
<TextView ... />
</LinearLayout>
</androidx.cardview.widget.CardView>
- The Concept: Android automatically grabs the top-most tag (the CardView) and saves it in a variable named
itemView. - "Whole Box": Since the CardView wraps everything (the image, the text, the background), attaching a click listener to
itemViewmeans clicking anywhere inside that card triggers the code. It saves you from manually findingR.id.cardRoot.
You absolutely can do that! It is a matter of coding style.
Option A: The Anonymous Way (What I showed)
- Style: Creating the listener "on the fly" inside
onViewCreated. - Pros: All your logic is in one place. You see the adapter creation and the click logic side-by-side.
- Cons:
onViewCreatedgets a bit long and messy.
Option B: The "Implements" Way (What you asked)
- Style: The Fragment class signs the contract.
- Pros: Cleaner
onViewCreated. - Cons: You have to scroll down to the bottom of the file to see what happens when you click.
How Option B looks:
// 1. Fragment implements the interface
public class PersonListFragment extends Fragment implements PersonGridAdapter.OnItemAction {
@Override
public void onViewCreated(...) {
// 2. Pass 'this' because the Fragment IS the listener now
adapter = new PersonGridAdapter(list, this);
}
// 3. The methods sit separately at the bottom
@Override
public void onPersonClick(Person p) { ... }
@Override
public void onImageClick(Person p) { ... }
}This is the trickiest part of RecyclerView logic.
The Problem: The "Index Shift"
Imagine a list of 5 items: [A, B, C, D, E]
- Indices: 0, 1, 2, 3, 4
You delete C (Index 2).
notifyItemRemoved(2):
- Visual: Row C vanishes with a cool animation.
- Internal Logic: RecyclerView knows C is gone.
- The Bug: RecyclerView still thinks D is at Index 3 and E is at Index 4. But in your actual Java List, D is now at Index 2!
notifyItemRangeChanged(2, list.size()):
- This tells RecyclerView: "Hey! Everything starting from Index 2 has shifted up. Please refresh their position numbers."
- If you don't do this, clicking on D might crash or delete the wrong item because the adapter still sends the old position.
| Action | Code | Why? |
|---|---|---|
| Delete Item | notifyItemRemoved(pos)notifyItemRangeChanged(pos, size) |
Mandatory. Fixes the index gap so future clicks are accurate. |
| Add Item (At End) | notifyItemInserted(list.size() - 1) |
One line enough. No one shifted positions; we just added to the tail. |
| Add Item (In Middle) | notifyItemInserted(pos)notifyItemRangeChanged(pos, size) |
Two lines. Items below the new one need to be pushed down (index +1). |
| Update Item | notifyItemChanged(pos) |
One line enough. No items moved; only the content (text/image) changed. |
| Lazy/Easy Way | notifyDataSetChanged() |
One line. Refreshes everything. Cons: No nice animations (flash effect). |