Module 06: RecyclerView & List Rendering
Objective: Master RecyclerView, Android's core list component, and understand the Adapter pattern and ViewHolder mechanism.
Frontend Comparison: List Rendering
| Frontend | Android | Description |
|---|---|---|
items.map(item => <Card />) | RecyclerView + Adapter | List rendering |
| Virtual scrolling (react-window) | RecyclerView (built-in) | Only render visible items |
| List item component | ViewHolder + item layout | Single list item |
key prop | getItemId() | Unique identifier |
| Pull-to-refresh | SwipeRefreshLayout | Refresh widget |
| Infinite scroll | OnScrollListener | Load more |
1. RecyclerView Core Concepts
RecyclerView is Android's "virtual list", similar to react-virtualized or react-window on the frontend. It only creates and renders views that are visible on screen, recycling existing ViewHolders as the user scrolls.
Three core components:
| Component | Responsibility | Frontend Analogy |
|---|---|---|
RecyclerView | Container; manages scrolling and recycling | List container component |
LayoutManager | Controls arrangement | CSS layout properties |
Adapter | Bridge between data and views | The mapping function in items.map() |
2. Basic Usage: Complete Example
2.1 Data Model
public class User {
private String name;
private String email;
private String avatarUrl;
public User(String name, String email, String avatarUrl) {
this.name = name;
this.email = email;
this.avatarUrl = avatarUrl;
}
public String getName() { return name; }
public String getEmail() { return email; }
public String getAvatarUrl() { return avatarUrl; }
}2.2 Item Layout
<!-- res/layout/item_user.xml -->
<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="16dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp" />
</LinearLayout>
</LinearLayout>2.3 Adapter Implementation
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserViewHolder> {
private List<User> users;
private OnItemClickListener listener;
// Click callback interface
public interface OnItemClickListener {
void onItemClick(User user, int position);
}
public UserAdapter(List<User> users, OnItemClickListener listener) {
this.users = users;
this.listener = listener;
}
// 1. Create ViewHolder (similar to creating a list item component)
@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new UserViewHolder(view);
}
// 2. Bind data to ViewHolder (similar to .map(item => render(item)))
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
User user = users.get(position);
holder.bind(user);
}
// 3. Return total item count
@Override
public int getItemCount() {
return users != null ? users.size() : 0;
}
// ViewHolder: holds references to all subviews within a list item
class UserViewHolder extends RecyclerView.ViewHolder {
private ImageView ivAvatar;
private TextView tvName;
private TextView tvEmail;
UserViewHolder(View itemView) {
super(itemView);
ivAvatar = itemView.findViewById(R.id.iv_avatar);
tvName = itemView.findViewById(R.id.tv_name);
tvEmail = itemView.findViewById(R.id.tv_email);
// Set up click event
itemView.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION && listener != null) {
listener.onItemClick(users.get(position), position);
}
});
}
void bind(User user) {
tvName.setText(user.getName());
tvEmail.setText(user.getEmail());
Glide.with(itemView.getContext())
.load(user.getAvatarUrl())
.circleCrop()
.into(ivAvatar);
}
}
}2.4 Using in Activity/Fragment
public class UserListActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private UserAdapter adapter;
private List<User> userList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
recyclerView = findViewById(R.id.rv_users);
// 1. Set LayoutManager (determines arrangement)
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 2. Create Adapter
adapter = new UserAdapter(userList, (user, position) -> {
// Handle click
Intent intent = new Intent(this, UserDetailActivity.class);
intent.putExtra("user_name", user.getName());
startActivity(intent);
});
// 3. Set Adapter
recyclerView.setAdapter(adapter);
// 4. Load data
loadUsers();
}
private void loadUsers() {
// Simulate network request
userList.add(new User("John", "john@example.com", "https://..."));
userList.add(new User("Jane", "jane@example.com", "https://..."));
userList.add(new User("Bob", "bob@example.com", "https://..."));
// Notify Adapter that data has changed (similar to setState triggering a re-render)
adapter.notifyDataSetChanged();
}
}2.5 Layout File
<!-- res/layout/activity_user_list.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="User List" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_users"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="16dp" />
</LinearLayout>3. LayoutManager Types
// Vertical list (most common)
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// Horizontal list
LinearLayoutManager hlm = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(hlm);
// Grid layout (similar to CSS Grid / display: grid)
recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); // 2 columns
// Staggered grid (similar to Pinterest-style layout)
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));4. Data Update Methods
// Full refresh (simple but inefficient)
adapter.notifyDataSetChanged();
// Precise updates (recommended, with animations)
// Insert an item
users.add(0, newUser);
adapter.notifyItemInserted(0);
// Remove an item
users.remove(position);
adapter.notifyItemRemoved(position);
// Update an item
users.set(position, updatedUser);
adapter.notifyItemChanged(position);
// Move an item
Collections.swap(users, fromPos, toPos);
adapter.notifyItemMoved(fromPos, toPos);
// Use DiffUtil (recommended -- automatically computes differences)
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() { return oldList.size(); }
@Override
public int getNewListSize() { return newList.size(); }
@Override
public boolean areItemsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).getId() == newList.get(newPos).getId();
}
@Override
public boolean areContentsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).equals(newList.get(newPos));
}
});
result.dispatchUpdatesTo(adapter);ListAdapter is recommended (built-in DiffUtil):
public class UserListAdapter extends ListAdapter<User, UserListAdapter.UserViewHolder> {
private static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(User oldItem, User newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(User oldItem, User newItem) {
return oldItem.getName().equals(newItem.getName())
&& oldItem.getEmail().equals(newItem.getEmail());
}
};
public UserListAdapter() {
super(DIFF_CALLBACK);
}
@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new UserViewHolder(view);
}
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
holder.bind(getItem(position));
}
// ... ViewHolder same as above
}
// Usage
UserListAdapter adapter = new UserListAdapter();
recyclerView.setAdapter(adapter);
// Update data (automatically computes diff and animates)
adapter.submitList(newUserList);5. Multi-Type Lists
public class MultiTypeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
private static final int TYPE_FOOTER = 2;
@Override
public int getItemViewType(int position) {
if (position == 0) return TYPE_HEADER;
if (position == getItemCount() - 1) return TYPE_FOOTER;
return TYPE_ITEM;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_HEADER:
View headerView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_header, parent, false);
return new HeaderViewHolder(headerView);
case TYPE_FOOTER:
View footerView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(footerView);
default:
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new ItemViewHolder(itemView);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HeaderViewHolder) {
((HeaderViewHolder) holder).bind();
} else if (holder instanceof ItemViewHolder) {
((ItemViewHolder) holder).bind(items.get(position - 1));
} else if (holder instanceof FooterViewHolder) {
((FooterViewHolder) holder).bind();
}
}
}6. Pull-to-Refresh + Load More on Scroll
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>SwipeRefreshLayout swipeRefresh = findViewById(R.id.swipe_refresh);
swipeRefresh.setOnRefreshListener(() -> {
// Pull-to-refresh
loadData(true);
});
// Load more on scroll to bottom
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
int totalItemCount = lm.getItemCount();
int lastVisibleItem = lm.findLastVisibleItemPosition();
// Trigger load more when reaching 3 items from the end
if (totalItemCount <= lastVisibleItem + 3 && !isLoading) {
loadMore();
}
}
});
private void loadData(boolean isRefresh) {
if (isRefresh) {
page = 1;
}
// Network request...
api.getUsers(page, new Callback() {
@Override
public void onSuccess(List<User> newUsers) {
if (isRefresh) {
userList.clear();
userList.addAll(newUsers);
adapter.notifyDataSetChanged();
} else {
int startPos = userList.size();
userList.addAll(newUsers);
adapter.notifyItemRangeInserted(startPos, newUsers.size());
}
swipeRefresh.setRefreshing(false);
isLoading = false;
}
});
}7. Adding Dividers & Decorations
// Add default divider
recyclerView.addItemDecoration(
new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
// Custom spacing decoration
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpaceItemDecoration(int space) {
this.space = space;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = space;
}
}
}
recyclerView.addItemDecoration(new SpaceItemDecoration(16));8. ItemTouchHelper: Swipe & Drag
// Swipe-to-delete + long-press drag-to-reorder
ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN, // Drag directions
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT // Swipe directions
) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
// Drag to reorder
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
Collections.swap(userList, from, to);
adapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// Swipe to delete
int position = viewHolder.getAdapterPosition();
userList.remove(position);
adapter.notifyItemRemoved(position);
}
});
helper.attachToRecyclerView(recyclerView);9. Legacy ListView + BaseAdapter Pattern
Old projects heavily use ListView and BaseAdapter, the predecessor of RecyclerView. Understanding it helps when reading legacy code:
// ListView-era adapter (ancestor of RecyclerView.Adapter)
// From actual tutorial project PlanetListAdapter.java
public class PlanetListAdapter extends BaseAdapter
implements AdapterView.OnItemClickListener {
private Context mContext;
private List<Planet> mPlanetList;
public PlanetListAdapter(Context context, List<Planet> planetList) {
mContext = context;
mPlanetList = planetList;
}
public int getCount() { return mPlanetList.size(); }
public Object getItem(int pos) { return mPlanetList.get(pos); }
public long getItemId(int pos) { return pos; }
// ⭐ getView = RecyclerView's onCreateViewHolder + onBindViewHolder combined
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) { // convertView is null, need to create
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list, null);
holder.ivIcon = convertView.findViewById(R.id.iv_icon);
holder.tvName = convertView.findViewById(R.id.tv_name);
holder.tvDesc = convertView.findViewById(R.id.tv_desc);
convertView.setTag(holder); // save ViewHolder to convertView
} else {
holder = (ViewHolder) convertView.getTag(); // reuse existing ViewHolder
}
// Bind data (similar to onBindViewHolder)
Planet planet = mPlanetList.get(position);
holder.ivIcon.setImageResource(planet.image);
holder.tvName.setText(planet.name);
holder.tvDesc.setText(planet.desc);
return convertView;
}
// ViewHolder pattern: cache child view references to avoid repeated findViewById
public final class ViewHolder {
public ImageView ivIcon;
public TextView tvName;
public TextView tvDesc;
}
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(mContext, "Clicked " + mPlanetList.get(position).name,
Toast.LENGTH_LONG).show();
}
}// Using ListView in an Activity (legacy pattern)
ListView listView = findViewById(R.id.lv_planet);
PlanetListAdapter adapter = new PlanetListAdapter(this, planetList);
listView.setAdapter(adapter); // set adapter
listView.setOnItemClickListener(adapter); // set click listenerListView vs RecyclerView Comparison:
| Feature | ListView (old) | RecyclerView (new) |
|---|---|---|
| ViewHolder | Manual, optional | Mandatory |
| Layout | Vertical list only | Flexible via LayoutManager |
| Animations | None built-in | Built-in add/remove/change |
| Partial refresh | Not supported | notifyItemXxx() |
| Click events | setOnItemClickListener | Manual in ViewHolder |
Migration tip: If you see
extends BaseAdapterorextends ArrayAdapter, it's a ListView-era adapter. To migrate to RecyclerView, change toextends RecyclerView.Adapterand splitgetView()intoonCreateViewHolder()+onBindViewHolder().
Frontend Developer Cheat Sheet
| Frontend List Concept | Android RecyclerView | Notes |
|---|---|---|
items.map(render) | onBindViewHolder() | Data-to-view mapping |
| List item component | ViewHolder | Recycling unit |
key prop | getItemId() + setHasStableIds(true) | Unique identifier |
| Virtual scrolling | Built-in (RecyclerView itself is a virtual list) | Performance optimization |
| List container | RecyclerView | Outer container |
display: grid | GridLayoutManager | Grid layout |
| Pull-to-refresh | SwipeRefreshLayout | Refresh container |
| Infinite scroll | OnScrollListener | Scroll listener |
setState(newItems) | adapter.notifyDataSetChanged() / submitList() | Data updates |
| Drag-to-reorder | ItemTouchHelper | Gesture interaction |
Next Steps
Now that you've mastered list rendering, move on to Module 07: Networking & Data Persistence to learn how to fetch and store data.