Android RecyclerView 多类型:从一个列表到多种 Item 的完整实践
本文主要讲解 Android 中 RecyclerView 多类型 Item 的实现思路,从普通单类型 RecyclerView 出发,逐步过渡到多 ViewHolder、多布局的完整案例,并总结常见坑和实际项目中的组织方式。
适合人群:已经会使用普通 RecyclerView,但还不清楚
getItemViewType()、多 ViewHolder 怎么写、实际项目怎么组织的 Android 初学者。
文章目录
- [Android RecyclerView 多类型:从一个列表到多种 Item 的完整实践](#Android RecyclerView 多类型:从一个列表到多种 Item 的完整实践)
-
- [一、前言:为什么需要多类型 RecyclerView?](#一、前言:为什么需要多类型 RecyclerView?)
- [二、先回顾普通 RecyclerView](#二、先回顾普通 RecyclerView)
- [三、问题来了:如果 Item 不一样怎么办?](#三、问题来了:如果 Item 不一样怎么办?)
- [四、多类型 RecyclerView 的核心思想](#四、多类型 RecyclerView 的核心思想)
- [五、完整案例:文本、图片、广告三种 Item](#五、完整案例:文本、图片、广告三种 Item)
-
- [1. 定义 Item 类型常量](#1. 定义 Item 类型常量)
- [2. 设计数据模型](#2. 设计数据模型)
- [3. 准备三种 Item 布局](#3. 准备三种 Item 布局)
-
- [普通文本 item_text.xml](#普通文本 item_text.xml)
- [图片 Item item_image.xml](#图片 Item item_image.xml)
- [广告 Item item_ad.xml](#广告 Item item_ad.xml)
- [4. 编写 Adapter 和三种 ViewHolder](#4. 编写 Adapter 和三种 ViewHolder)
- [5. Activity 中使用](#5. Activity 中使用)
- [六、最关键的 getItemViewType()](#六、最关键的 getItemViewType())
- [七、position 和 viewType 不是一回事](#七、position 和 viewType 不是一回事)
- [八、RecyclerView 到底是怎么运行的?](#八、RecyclerView 到底是怎么运行的?)
- 九、常见坑与注意事项
-
- [1. RecyclerView 不显示](#1. RecyclerView 不显示)
- [2. viewType 可以随便写吗?](#2. viewType 可以随便写吗?)
- [3. 为什么要抛出异常?](#3. 为什么要抛出异常?)
- [4. 不要用多个 RecyclerView 模拟多类型](#4. 不要用多个 RecyclerView 模拟多类型)
- [十、实际项目中怎么组织多类型 RecyclerView?](#十、实际项目中怎么组织多类型 RecyclerView?)
一、前言:为什么需要多类型 RecyclerView?
在实际 Android 项目中,RecyclerView 很少永远只有一种布局。
例如一个新闻 App首页有很多模块的内容, 可能有普通新闻, 带大图的新闻, 广告, 还有推荐内容等. 如果 RecyclerView 只能使用一种 Item 布局,那么这种需求就会变得很麻烦。所以这一篇主要学习 RecyclerView 如何在同一个列表中显示不同类型的 Item?
二、先回顾普通 RecyclerView
在学习多类型之前,先回忆一下普通 RecyclerView。例如我们有一个用户列表:
RecyclerView 会不断重复利用 Item。Adapter 中最重要的几个方法, 可以简单理解成下面的表格
| 方法 | 作用 |
|---|---|
onCreateViewHolder() |
我要创建一个什么样子的 Item? |
onBindViewHolder() |
这个 Item 里面应该显示什么数据? |
getItemCount() |
总共有多少个 Item? |
三、问题来了:如果 Item 不一样怎么办?
我们希望 RecyclerView 最终显示:
text
┌─────────────────────┐
│ 普通文本 │
└─────────────────────┘
┌─────────────────────┐
│ 图片 │
│ 图片标题 │
└─────────────────────┘
┌─────────────────────┐
│ 广告 │
└─────────────────────┘
┌─────────────────────┐
│ 普通文本 │
└─────────────────────┘
这时候问题就出现了, onCreateViewHolder() 到底应该创建哪一种布局?我们需要根据 Item 的类型决定。这就是多类型 RecyclerView 的核心。
四、多类型 RecyclerView 的核心思想
多类型 RecyclerView 并不是出现了什么新的 RecyclerView。它本质上还是RecyclerView->Adapter->ViewHolder
只不过:一个 Adapter 不再只有一种 ViewHolder,而是拥有多个 ViewHolder。
Adapter
├── TextViewHolder
├── ImageViewHolder
└── AdViewHolder
而 RecyclerView 会通过一个叫 getItemViewType() 的方法判断: 当前位置的 Item 属于什么类型?整体流程如下所示, 核心是通过判断Item的类型进行后续不同的操作
position
↓
getItemViewType(position)
↓
得到 Item 类型
↓
onCreateViewHolder()
↓
创建对应 ViewHolder
↓
onBindViewHolder()
↓
绑定对应数据
五、完整案例:文本、图片、广告三种 Item
下面做一个简单但完整的例子。
RecyclerView 中有三种 Item:
TYPE_TEXT 普通文本
TYPE_IMAGE 图片 + 标题
TYPE_AD 广告
1. 定义 Item 类型常量
在 Adapter 中定义三个常量:
java
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
public static final int TYPE_AD = 2;
不要直接写数字,例如 0、1、2。 使用常量可读性更好,也更方便维护。
2. 设计数据模型
这里有一个非常重要的思想:Item 类型最好成为数据的一部分。
java
public class ItemData {
private final int type;
private final String title;
public ItemData(int type, String title) {
this.type = type;
this.title = title;
}
public int getType() {
return type;
}
public String getTitle() {
return title;
}
}
这样我们就可以通过下面的方法创建变量添加到list中
java
List<ItemData> dataList = new ArrayList<>();
dataList.add(new ItemData(MultiTypeAdapter.TYPE_TEXT, "这是普通文本"));
dataList.add(new ItemData(MultiTypeAdapter.TYPE_IMAGE, "这是图片新闻"));
dataList.add(new ItemData(MultiTypeAdapter.TYPE_AD, "这是一个广告"));
dataList.add(new ItemData(MultiTypeAdapter.TYPE_TEXT, "第二条普通文本"));
数据本身已经告诉 Adapter这个 Item 是什么类型;这个 Item 的内容是什么。
3. 准备三种 Item 布局
普通文本 item_text.xml
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="60dp"
android:gravity="center_vertical"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp" />
</LinearLayout>
图片 Item item_image.xml
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="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:id="@+id/iv_image"
android:layout_width="match_parent"
android:layout_height="90dp"
android:background="#CCCCCC"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp" />
</LinearLayout>
这里为了让案例聚焦多类型,图片 Item 暂时不加载网络图片,只设置一个灰色占位背景。
广告 Item item_ad.xml
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="80dp"
android:background="#EEEEEE"
android:gravity="center">
<TextView
android:id="@+id/tv_ad"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="广告"
android:textSize="18sp" />
</LinearLayout>
4. 编写 Adapter 和三种 ViewHolder
完整 Adapter 如下。
java
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class MultiTypeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
public static final int TYPE_AD = 2;
private final List<ItemData> dataList;
public MultiTypeAdapter(List<ItemData> dataList) {
this.dataList = dataList;
}
@Override
public int getItemViewType(int position) {
return dataList.get(position).getType();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case TYPE_TEXT:
return new TextViewHolder(
inflater.inflate(R.layout.item_text, parent, false)
);
case TYPE_IMAGE:
return new ImageViewHolder(
inflater.inflate(R.layout.item_image, parent, false)
);
case TYPE_AD:
return new AdViewHolder(
inflater.inflate(R.layout.item_ad, parent, false)
);
default:
throw new IllegalArgumentException("Unknown viewType: " + viewType);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ItemData item = dataList.get(position);
int viewType = holder.getItemViewType();
switch (viewType) {
case TYPE_TEXT:
((TextViewHolder) holder).tvTitle.setText(item.getTitle());
break;
case TYPE_IMAGE:
((ImageViewHolder) holder).tvTitle.setText(item.getTitle());
break;
case TYPE_AD:
((AdViewHolder) holder).tvAd.setText(item.getTitle());
break;
default:
throw new IllegalArgumentException("Unknown viewType: " + viewType);
}
}
@Override
public int getItemCount() {
return dataList.size();
}
static class TextViewHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
public TextViewHolder(View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tv_title);
}
}
static class ImageViewHolder extends RecyclerView.ViewHolder {
ImageView ivImage;
TextView tvTitle;
public ImageViewHolder(View itemView) {
super(itemView);
ivImage = itemView.findViewById(R.id.iv_image);
tvTitle = itemView.findViewById(R.id.tv_title);
}
}
static class AdViewHolder extends RecyclerView.ViewHolder {
TextView tvAd;
public AdViewHolder(View itemView) {
super(itemView);
tvAd = itemView.findViewById(R.id.tv_ad);
}
}
}
到这里,一个最基本的多类型 RecyclerView 就完成了。
5. Activity 中使用
activity_main.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
MainActivity.java:
java
import android.os.Bundle;
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 {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
List<ItemData> dataList = new ArrayList<>();
dataList.add(new ItemData(MultiTypeAdapter.TYPE_TEXT, "这是第一条文本"));
dataList.add(new ItemData(MultiTypeAdapter.TYPE_IMAGE, "这是一条图片新闻"));
dataList.add(new ItemData(MultiTypeAdapter.TYPE_AD, "这里是广告"));
dataList.add(new ItemData(MultiTypeAdapter.TYPE_TEXT, "这是第二条文本"));
MultiTypeAdapter adapter = new MultiTypeAdapter(dataList);
recyclerView.setAdapter(adapter);
}
}
运行之后:
普通文本
↓
图片新闻
↓
广告
↓
普通文本
不同的 Item 就可以同时存在于同一个 RecyclerView 中。
六、最关键的 getItemViewType()
现在进入多类型 RecyclerView 最重要的地方。
java
@Override
public int getItemViewType(int position) {
return dataList.get(position).getType();
}
看起来非常简单。但是这行代码实际上完成了一件非常重要的事情, 那就是读取Item返回类型数据
七、position 和 viewType 不是一回事
这是刚开始学习多类型 RecyclerView 时非常容易混淆的地方。
| position | type |
|---|---|
| 0 | TEXT |
| 1 | IMAGE |
| 2 | AD |
| 3 | TEXT |
| 4 | IMAGE |
position 表示当前 Item 在列表中的位置。而 viewType 表示当前 Item 应该使用什么样的布局。
所以position = 3并不意味着viewType = 3
八、RecyclerView 到底是怎么运行的?
现在可以把整个过程完整串起来。
假设:
dataList:
0 → TEXT
1 → IMAGE
2 → AD
3 → TEXT
RecyclerView 需要显示 position = 1首先调用:getItemViewType(1). 然后调用onCreateViewHolder(parent, TYPE_IMAGE). 通过Adapter创建ImageViewHolder
然后是绑定数据:onBindViewHolder(holder, 1)
最终显示出图片新闻. 整个过程可以记成:
position
↓
getItemViewType()
↓
viewType
↓
onCreateViewHolder()
↓
ViewHolder
↓
onBindViewHolder()
↓
数据
这就是多类型 RecyclerView 的核心运行逻辑。
九、常见坑与注意事项
1. RecyclerView 不显示
最常见原因:
java
recyclerView.setLayoutManager(new LinearLayoutManager(this));
忘记设置 LayoutManager。RecyclerView 如果没有 LayoutManager,是不会显示内容的。
2. viewType 可以随便写吗?
可以使用0、1、2、3
但是更推荐下面的宏定义写法, 这样代码可读性明显更好。
java
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
public static final int TYPE_AD = 2;
3. 为什么要抛出异常?
java
throw new IllegalArgumentException("Unknown viewType: " + viewType);
因为如果未来增加了一种类型,却忘记在 Adapter 中处理,程序应该尽早告诉你这个 viewType 没有对应的 ViewHolder。
4. 不要用多个 RecyclerView 模拟多类型
很多初学者看到不同布局之后,第一反应可能是:
text
RecyclerView 1 → 第一种 Item
RecyclerView 2 → 第二种 Item
RecyclerView 3 → 第三种 Item
通常没必要。
一个 RecyclerView 可以管理多种 Item。
这正是 getItemViewType() 存在的意义。
十、实际项目中怎么组织多类型 RecyclerView?
当类型少的时候,一个 Adapter 加几个 ViewHolder 就够用。
但如果一个真实项目有10 种, 20 种或30 种
继续写:
java
if (viewType == TYPE_TEXT) {
...
} else if (viewType == TYPE_IMAGE) {
...
} else if (viewType == TYPE_AD) {
...
} else if (...) {
...
}
Adapter 会越来越难维护。所以实际项目通常会进一步进行封装。常见组织方式如下
简单项目
一个 Adapter
├── TextViewHolder
├── ImageViewHolder
└── AdViewHolder
适合类型少、逻辑简单的页面。
中等项目
Adapter
↓
ItemBinder / Delegate
├── TextBinder
├── ImageBinder
└── AdBinder
每种类型独立管理自己的创建和绑定逻辑。
复杂项目
可以结合:
ListAdapterDiffUtilViewBindingConcatAdapter- 分页库 Paging
让 Adapter 只负责分发,具体 Item 逻辑拆出去。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注。后续会继续更新 RecyclerView 点击事件、DiffUtil、ListAdapter、ViewBinding 以及仿写小红书首页相关内容。