Skip to content

模块 06:RecyclerView 与列表渲染

目标:掌握 Android 中最核心的列表组件 RecyclerView,理解 Adapter 模式和 ViewHolder 机制。

前端对照:列表渲染

前端Android说明
items.map(item => <Card />)RecyclerView + Adapter列表渲染
虚拟滚动 (react-window)RecyclerView (内置)只渲染可见项
列表项组件ViewHolder + item 布局单个列表项
key 属性getItemId()唯一标识
下拉刷新SwipeRefreshLayout刷新控件
无限滚动OnScrollListener加载更多

1. RecyclerView 核心概念

RecyclerView 是 Android 的"虚拟列表",类似前端的 react-virtualized 或 react-window。它只创建和渲染屏幕可见范围内的视图,滚动时复用已有的 ViewHolder。

三个核心组件

组件职责前端类比
RecyclerView容器,管理滚动和回收列表容器组件
LayoutManager控制排列方式CSS layout 属性
Adapter数据和视图的桥梁items.map() 的映射函数

2. 基础用法:完整示例

2.1 数据模型

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 列表项布局

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 实现

java
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserViewHolder> {

    private List<User> users;
    private OnItemClickListener listener;

    // 点击回调接口
    public interface OnItemClickListener {
        void onItemClick(User user, int position);
    }

    public UserAdapter(List<User> users, OnItemClickListener listener) {
        this.users = users;
        this.listener = listener;
    }

    // ① 创建 ViewHolder(类似创建列表项组件)
    @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);
    }

    // ② 绑定数据到 ViewHolder(类似 .map(item => render(item)))
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = users.get(position);
        holder.bind(user);
    }

    // ③ 返回数据总数
    @Override
    public int getItemCount() {
        return users != null ? users.size() : 0;
    }

    // ViewHolder:持有列表项中所有子视图的引用
    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);

            // 设置点击事件
            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 在 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. 设置 LayoutManager(决定排列方式)
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        // 2. 创建 Adapter
        adapter = new UserAdapter(userList, (user, position) -> {
            // 处理点击
            Intent intent = new Intent(this, UserDetailActivity.class);
            intent.putExtra("user_name", user.getName());
            startActivity(intent);
        });

        // 3. 设置 Adapter
        recyclerView.setAdapter(adapter);

        // 4. 加载数据
        loadUsers();
    }

    private void loadUsers() {
        // 模拟网络请求
        userList.add(new User("张三", "zhangsan@example.com", "https://..."));
        userList.add(new User("李四", "lisi@example.com", "https://..."));
        userList.add(new User("王五", "wangwu@example.com", "https://..."));

        // 通知 Adapter 数据已变化(类似 setState 触发重新渲染)
        adapter.notifyDataSetChanged();
    }
}

2.5 布局文件

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="用户列表" />

    <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 类型

java
// 垂直列表(最常用)
recyclerView.setLayoutManager(new LinearLayoutManager(this));

// 水平列表
LinearLayoutManager hlm = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(hlm);

// 网格布局(类似 CSS Grid / display: grid)
recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); // 2列

// 瀑布流(类似 Pinterest 布局)
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));

4. 数据更新方法

java
// 全量刷新(简单但低效)
adapter.notifyDataSetChanged();

// 精确更新(推荐,带动画效果)
// 插入一项
users.add(0, newUser);
adapter.notifyItemInserted(0);

// 删除一项
users.remove(position);
adapter.notifyItemRemoved(position);

// 修改一项
users.set(position, updatedUser);
adapter.notifyItemChanged(position);

// 移动一项
Collections.swap(users, fromPos, toPos);
adapter.notifyItemMoved(fromPos, toPos);

// 使用 DiffUtil(推荐,自动计算差异)
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(内置 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 同上
}

// 使用
UserListAdapter adapter = new UserListAdapter();
recyclerView.setAdapter(adapter);

// 更新数据(自动计算差异并动画)
adapter.submitList(newUserList);

5. 多类型列表

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. 下拉刷新 + 上拉加载更多

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(() -> {
    // 下拉刷新
    loadData(true);
});

// 上拉加载更多
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();

        // 滑到倒数第 3 项时触发加载更多
        if (totalItemCount <= lastVisibleItem + 3 && !isLoading) {
            loadMore();
        }
    }
});

private void loadData(boolean isRefresh) {
    if (isRefresh) {
        page = 1;
    }
    // 网络请求...
    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. 添加分割线和装饰

java
// 添加默认分割线
recyclerView.addItemDecoration(
    new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

// 自定义间距装饰
public classSpaceItemDecoration 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:滑动和拖拽

java
// 左滑删除 + 长按拖拽排序
ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP | ItemTouchHelper.DOWN,    // 拖拽方向
    ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT   // 滑动方向
) {
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
                          RecyclerView.ViewHolder target) {
        // 拖拽排序
        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) {
        // 滑动删除
        int position = viewHolder.getAdapterPosition();
        userList.remove(position);
        adapter.notifyItemRemoved(position);
    }
});
helper.attachToRecyclerView(recyclerView);

9. 老项目中的 ListView + BaseAdapter 模式

老项目中大量使用 ListView 和 BaseAdapter,它是 RecyclerView 的前身。理解它有助于阅读老代码:

java
// ListView 时代的适配器(类似 RecyclerView.Adapter 的祖先)
// 来自实际教程项目 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 的 onCreateViewHolder + onBindViewHolder 合并体
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {   // 转换视图为空,需要创建
            holder = new ViewHolder();
            // 根据布局文件生成视图(类似 RecyclerView 的 inflate)
            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);   // 将 ViewHolder 保存到 convertView
        } else {
            holder = (ViewHolder) convertView.getTag(); // 复用已有 ViewHolder
        }
        // 绑定数据(类似 onBindViewHolder)
        Planet planet = mPlanetList.get(position);
        holder.ivIcon.setImageResource(planet.image);
        holder.tvName.setText(planet.name);
        holder.tvDesc.setText(planet.desc);
        return convertView;
    }

    // ViewHolder 模式:缓存列表项的子视图引用,避免重复 findViewById
    public final class ViewHolder {
        public ImageView ivIcon;
        public TextView tvName;
        public TextView tvDesc;
    }

    // 处理列表项点击(类似 RecyclerView 的 OnItemClickListener)
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Toast.makeText(mContext, "点击了" + mPlanetList.get(position).name,
            Toast.LENGTH_LONG).show();
    }
}
java
// 在 Activity 中使用 ListView(老项目写法)
ListView listView = findViewById(R.id.lv_planet);
PlanetListAdapter adapter = new PlanetListAdapter(this, planetList);
listView.setAdapter(adapter);               // 设置适配器
listView.setOnItemClickListener(adapter);    // 设置点击监听

ListView vs RecyclerView 对比

特性ListView (老)RecyclerView (新)
ViewHolder手动实现,可选强制使用
布局方式只能垂直列表LayoutManager 灵活控制
动画无内置内置增删改动画
局部刷新不支持notifyItemXxx()
点击事件setOnItemClickListenerViewHolder 中手动设置

老项目迁移提示:如果你看到 extends BaseAdapterextends ArrayAdapter,这就是 ListView 时代的适配器。迁移到 RecyclerView 时,需要改为 extends RecyclerView.Adapter 并拆分 getView()onCreateViewHolder() + onBindViewHolder()

前端开发者备忘

前端列表概念Android RecyclerView备注
items.map(render)onBindViewHolder()数据到视图的映射
列表项组件ViewHolder复用单元
key propgetItemId() + setHasStableIds(true)唯一标识
虚拟滚动内置(RecyclerView 本身就是虚拟列表)性能优化
列表容器RecyclerView外层容器
display: gridGridLayoutManager网格布局
下拉刷新SwipeRefreshLayout刷新容器
无限滚动OnScrollListener滚动监听
setState(newItems)adapter.notifyDataSetChanged() / submitList()数据更新
拖拽排序ItemTouchHelper手势交互

下一步

掌握了列表渲染后,接下来学习 模块 07:网络请求与数据持久化,学习如何获取和存储数据。

面向前端开发者的 Java Android 开发教程