Skip to content

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

FrontendAndroidDescription
items.map(item => <Card />)RecyclerView + AdapterList rendering
Virtual scrolling (react-window)RecyclerView (built-in)Only render visible items
List item componentViewHolder + item layoutSingle list item
key propgetItemId()Unique identifier
Pull-to-refreshSwipeRefreshLayoutRefresh widget
Infinite scrollOnScrollListenerLoad 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:

ComponentResponsibilityFrontend Analogy
RecyclerViewContainer; manages scrolling and recyclingList container component
LayoutManagerControls arrangementCSS layout properties
AdapterBridge between data and viewsThe mapping function in items.map()

2. Basic Usage: Complete Example

2.1 Data Model

java
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

xml
<!-- 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

java
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

java
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

xml
<!-- 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

java
// 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

java
// 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):

java
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

java
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

xml
<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>
java
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

java
// 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

java
// 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:

java
// 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();
    }
}
java
// 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 listener

ListView vs RecyclerView Comparison:

FeatureListView (old)RecyclerView (new)
ViewHolderManual, optionalMandatory
LayoutVertical list onlyFlexible via LayoutManager
AnimationsNone built-inBuilt-in add/remove/change
Partial refreshNot supportednotifyItemXxx()
Click eventssetOnItemClickListenerManual in ViewHolder

Migration tip: If you see extends BaseAdapter or extends ArrayAdapter, it's a ListView-era adapter. To migrate to RecyclerView, change to extends RecyclerView.Adapter and split getView() into onCreateViewHolder() + onBindViewHolder().

Frontend Developer Cheat Sheet

Frontend List ConceptAndroid RecyclerViewNotes
items.map(render)onBindViewHolder()Data-to-view mapping
List item componentViewHolderRecycling unit
key propgetItemId() + setHasStableIds(true)Unique identifier
Virtual scrollingBuilt-in (RecyclerView itself is a virtual list)Performance optimization
List containerRecyclerViewOuter container
display: gridGridLayoutManagerGrid layout
Pull-to-refreshSwipeRefreshLayoutRefresh container
Infinite scrollOnScrollListenerScroll listener
setState(newItems)adapter.notifyDataSetChanged() / submitList()Data updates
Drag-to-reorderItemTouchHelperGesture 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.

A Java Android development tutorial for frontend developers