Introduction to RecyclerView
RecyclerView is a more advanced and flexible version of ListView for displaying large data sets efficiently. It recycles item views as they scroll off-screen, providing better performance and memory management.
Basic RecyclerView Setup
1. Add Dependencies
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
}
2. Layout Files
activity_main.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="match_parent" android:orientation="vertical" android:padding="16dp"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
item_layout.xml (Individual item layout)
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView 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:layout_margin="8dp" app:cardCornerRadius="8dp" app:cardElevation="4dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="16dp"> <ImageView android:id="@+id/imageView" android:layout_width="60dp" android:layout_height="60dp" android:src="@mipmap/ic_launcher" /> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginStart="16dp" android:orientation="vertical"> <TextView android:id="@+id/textTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Title" android:textSize="18sp" android:textStyle="bold" /> <TextView android:id="@+id/textDescription" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Description" android:textSize="14sp" android:layout_marginTop="4dp" /> </LinearLayout> <ImageButton android:id="@+id/btnDelete" android:layout_width="40dp" android:layout_height="40dp" android:src="@android:drawable/ic_delete" android:background="?android:attr/selectableItemBackground" /> </LinearLayout> </androidx.cardview.widget.CardView>
Data Model Class
public class Item {
private String title;
private String description;
private int imageResource;
private boolean isSelected;
public Item(String title, String description, int imageResource) {
this.title = title;
this.description = description;
this.imageResource = imageResource;
this.isSelected = false;
}
// Getters and Setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public int getImageResource() { return imageResource; }
public void setImageResource(int imageResource) { this.imageResource = imageResource; }
public boolean isSelected() { return isSelected; }
public void setSelected(boolean selected) { isSelected = selected; }
}
ViewHolder Pattern
Basic ViewHolder
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class ItemViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;
public TextView textTitle;
public TextView textDescription;
public ImageButton btnDelete;
public ItemViewHolder(@NonNull View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.imageView);
textTitle = itemView.findViewById(R.id.textTitle);
textDescription = itemView.findViewById(R.id.textDescription);
btnDelete = itemView.findViewById(R.id.btnDelete);
}
}
RecyclerView Adapter
Basic Adapter Implementation
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
private List<Item> itemList;
private OnItemClickListener listener;
// Interface for click events
public interface OnItemClickListener {
void onItemClick(int position);
void onDeleteClick(int position);
}
public ItemAdapter(List<Item> itemList, OnItemClickListener listener) {
this.itemList = itemList;
this.listener = listener;
}
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_layout, parent, false);
return new ItemViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
Item currentItem = itemList.get(position);
holder.imageView.setImageResource(currentItem.getImageResource());
holder.textTitle.setText(currentItem.getTitle());
holder.textDescription.setText(currentItem.getDescription());
// Handle item click
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onItemClick(position);
}
});
// Handle delete button click
holder.btnDelete.setOnClickListener(v -> {
if (listener != null) {
listener.onDeleteClick(position);
}
});
}
@Override
public int getItemCount() {
return itemList.size();
}
}
Advanced Adapter with Multiple View Types
Multiple View Types Adapter
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class MultiTypeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Object> items;
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
private static final int TYPE_FOOTER = 2;
public MultiTypeAdapter(List<Object> items) {
this.items = items;
}
@Override
public int getItemViewType(int position) {
Object item = items.get(position);
if (item instanceof Header) {
return TYPE_HEADER;
} else if (item instanceof Footer) {
return TYPE_FOOTER;
} else {
return TYPE_ITEM;
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case TYPE_HEADER:
View headerView = inflater.inflate(R.layout.header_layout, parent, false);
return new HeaderViewHolder(headerView);
case TYPE_FOOTER:
View footerView = inflater.inflate(R.layout.footer_layout, parent, false);
return new FooterViewHolder(footerView);
default:
View itemView = inflater.inflate(R.layout.item_layout, parent, false);
return new ItemViewHolder(itemView);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case TYPE_HEADER:
Header header = (Header) items.get(position);
HeaderViewHolder headerHolder = (HeaderViewHolder) holder;
headerHolder.textHeader.setText(header.getTitle());
break;
case TYPE_ITEM:
Item item = (Item) items.get(position);
ItemViewHolder itemHolder = (ItemViewHolder) holder;
itemHolder.textTitle.setText(item.getTitle());
itemHolder.textDescription.setText(item.getDescription());
break;
case TYPE_FOOTER:
Footer footer = (Footer) items.get(position);
FooterViewHolder footerHolder = (FooterViewHolder) holder;
footerHolder.textFooter.setText(footer.getMessage());
break;
}
}
@Override
public int getItemCount() {
return items.size();
}
// Different ViewHolder classes
static class HeaderViewHolder extends RecyclerView.ViewHolder {
TextView textHeader;
HeaderViewHolder(View view) {
super(view);
textHeader = view.findViewById(R.id.textHeader);
}
}
static class FooterViewHolder extends RecyclerView.ViewHolder {
TextView textFooter;
FooterViewHolder(View view) {
super(view);
textFooter = view.findViewById(R.id.textFooter);
}
}
}
DiffUtil for Efficient Updates
Using DiffUtil for Better Performance
import androidx.recyclerview.widget.DiffUtil;
import java.util.List;
public class ItemDiffCallback extends DiffUtil.Callback {
private final List<Item> oldList;
private final List<Item> newList;
public ItemDiffCallback(List<Item> oldList, List<Item> newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
Item oldItem = oldList.get(oldItemPosition);
Item newItem = newList.get(newItemPosition);
return oldItem.getTitle().equals(newItem.getTitle());
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
Item oldItem = oldList.get(oldItemPosition);
Item newItem = newList.get(newItemPosition);
return oldItem.getTitle().equals(newItem.getTitle()) &&
oldItem.getDescription().equals(newItem.getDescription()) &&
oldItem.getImageResource() == newItem.getImageResource();
}
}
// Enhanced Adapter with DiffUtil
public class EfficientItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
private List<Item> items;
private OnItemClickListener listener;
public EfficientItemAdapter(List<Item> items, OnItemClickListener listener) {
this.items = items;
this.listener = listener;
}
public void updateItems(List<Item> newItems) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new ItemDiffCallback(this.items, newItems)
);
this.items.clear();
this.items.addAll(newItems);
diffResult.dispatchUpdatesTo(this);
}
// ... rest of the adapter methods
}
Complete MainActivity Implementation
import android.os.Bundle;
import android.widget.Toast;
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 MainActivity extends AppCompatActivity implements ItemAdapter.OnItemClickListener {
private RecyclerView recyclerView;
private ItemAdapter adapter;
private List<Item> itemList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initializeData();
setupRecyclerView();
}
private void initializeData() {
itemList = new ArrayList<>();
itemList.add(new Item("Java", "Object-oriented programming language", R.drawable.java_icon));
itemList.add(new Item("Kotlin", "Modern programming language for Android", R.drawable.kotlin_icon));
itemList.add(new Item("Python", "High-level programming language", R.drawable.python_icon));
itemList.add(new Item("JavaScript", "Scripting language for web development", R.drawable.js_icon));
itemList.add(new Item("C++", "General-purpose programming language", R.drawable.cpp_icon));
// Add more items as needed
}
private void setupRecyclerView() {
recyclerView = findViewById(R.id.recyclerView);
// Set layout manager
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
// Set adapter
adapter = new ItemAdapter(itemList, this);
recyclerView.setAdapter(adapter);
// Add item decoration for spacing
recyclerView.addItemDecoration(new ItemDecoration(16));
}
@Override
public void onItemClick(int position) {
Item item = itemList.get(position);
Toast.makeText(this, "Clicked: " + item.getTitle(), Toast.LENGTH_SHORT).show();
// Toggle selection
item.setSelected(!item.isSelected());
adapter.notifyItemChanged(position);
}
@Override
public void onDeleteClick(int position) {
Item item = itemList.get(position);
itemList.remove(position);
adapter.notifyItemRemoved(position);
Toast.makeText(this, "Deleted: " + item.getTitle(), Toast.LENGTH_SHORT).show();
}
// Method to add new item
public void addItem(Item newItem) {
itemList.add(0, newItem); // Add to beginning
adapter.notifyItemInserted(0);
recyclerView.scrollToPosition(0);
}
// Method to update item
public void updateItem(int position, Item updatedItem) {
itemList.set(position, updatedItem);
adapter.notifyItemChanged(position);
}
}
Custom Item Decoration
import android.graphics.Rect;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class ItemDecoration extends RecyclerView.ItemDecoration {
private final int spacing;
public ItemDecoration(int spacing) {
this.spacing = spacing;
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
outRect.left = spacing;
outRect.right = spacing;
outRect.bottom = spacing;
// Add top margin only for the first item
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = spacing;
}
}
}
Advanced Features
1. Swipe to Delete
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
public class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback {
private ItemAdapter adapter;
public SwipeToDeleteCallback(ItemAdapter adapter) {
super(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
this.adapter = adapter;
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
return false; // We don't support drag & drop
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
adapter.deleteItem(position);
}
}
// Usage in MainActivity
private void enableSwipeToDelete() {
SwipeToDeleteCallback swipeToDeleteCallback = new SwipeToDeleteCallback(adapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(swipeToDeleteCallback);
itemTouchHelper.attachToRecyclerView(recyclerView);
}
2. Drag and Drop
public class DragDropCallback extends ItemTouchHelper.Callback {
private final ItemAdapter adapter;
public DragDropCallback(ItemAdapter adapter) {
this.adapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
return makeMovementFlags(dragFlags, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder source,
@NonNull RecyclerView.ViewHolder target) {
adapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// Not used
}
}
// Add this method to your adapter
public void onItemMove(int fromPosition, int toPosition) {
Collections.swap(itemList, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
}
3. Endless Scrolling
public abstract class EndlessScrollListener extends RecyclerView.OnScrollListener {
private LinearLayoutManager layoutManager;
private int visibleThreshold = 5;
private int currentPage = 0;
private int previousTotalItemCount = 0;
private boolean loading = true;
private int startingPageIndex = 0;
public EndlessScrollListener(LinearLayoutManager layoutManager) {
this.layoutManager = layoutManager;
}
@Override
public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
int totalItemCount = layoutManager.getItemCount();
if (totalItemCount < previousTotalItemCount) {
this.currentPage = this.startingPageIndex;
this.previousTotalItemCount = totalItemCount;
if (totalItemCount == 0) {
this.loading = true;
}
}
if (loading && (totalItemCount > previousTotalItemCount)) {
loading = false;
previousTotalItemCount = totalItemCount;
}
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
currentPage++;
onLoadMore(currentPage, totalItemCount, view);
loading = true;
}
}
public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
}
// Usage
recyclerView.addOnScrollListener(new EndlessScrollListener(layoutManager) {
@Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
// Load more data
loadMoreData(page);
}
});
Best Practices and Performance Tips
1. ViewHolder Pattern
- Always use ViewHolder pattern
- Store view references in ViewHolder
- Avoid findViewById() in onBindViewHolder()
2. Efficient Data Updates
- Use DiffUtil for large datasets
- Prefer notifyItemChanged() over notifyDataSetChanged()
- Use stable IDs when possible
3. Memory Management
- Clear references in onViewRecycled()
- Use weak references for click listeners
- Implement proper lifecycle management
4. Layout Optimization
- Use ConstraintLayout for complex item layouts
- Avoid nested weights
- Use appropriate view types
Common Issues and Solutions
1. Item Click Not Working
// Make sure items are clickable holder.itemView.setClickable(true); // Or set focusable if needed holder.itemView.setFocusable(true);
2. RecyclerView Not Showing
// Check layout manager is set
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// Check adapter is set
recyclerView.setAdapter(adapter);
// Check data is not empty
if (itemList.isEmpty()) {
// Show empty state
}
3. Performance Issues
// Use setHasFixedSize(true) if item size doesn't change recyclerView.setHasFixedSize(true); // Use setItemViewCacheSize() for better scrolling recyclerView.setItemViewCacheSize(20); // Use setRecycledViewPool() for multiple RecyclerViews RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool(); recyclerView.setRecycledViewPool(sharedPool);
RecyclerView with custom adapters provides a powerful and efficient way to display lists and grids in Android applications. By following these patterns and best practices, you can create smooth, responsive list interfaces that handle large datasets efficiently.