目录
2.2 场景 2:图文混合列表(SimpleAdapter)
2.3 场景 3:自定义布局 + 交互(BaseAdapter)
4.1 必做优化:ViewHolder 模式(已讲过,再强调)
五、避坑指南:解决 ListView 开发中最常见的 8 个问题
5.2 问题 2:notifyDataSetChanged () 不生效
5.4 问题 4:ListView 没有滑动到底部却触发了分页加载
5.6 问题 6:ListView 滑动卡顿,日志显示 “跳过多少帧”
5.8 问题 8:ListView 数据更新后,滚动位置重置到顶部
7.2 ListView 与 RecyclerView 对比

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
前言
作为 Android 开发中最经典的列表控件,ListView 承载了无数 App 的核心展示功能 —— 从早期的短信列表、联系人页面,到如今的商品列表、新闻信息流,都能看到它的身影。虽然现在 RecyclerView 逐渐成为主流,但 ListView 依然是新手入门的必经之路,也是很多 legacy 项目的核心组件。

很多开发者对 ListView 的印象停留在 “简单但卡顿”,其实只要掌握了核心原理和优化技巧,它同样能实现丝滑流畅的列表体验。本文会从基础用法到进阶技巧,再到性能优化和避坑指南,用通俗易懂的语言 + 可直接运行的代码,带你彻底吃透 ListView,无论是新手入门还是老手重构旧项目,都能有所收获。
一、先搞懂:ListView 到底是什么?
新手刚接触时,很容易把 ListView 和 Adapter 搞混,其实一句话就能理清关系:ListView 是 “容器”,负责展示列表视图;Adapter 是 “桥梁”,负责把数据转换成视图并交给 ListView 展示。

1.1 ListView 的核心作用
ListView 本质是一个 “可滚动的线性布局容器”,专门用于高效展示大量同类数据。它的核心优势是视图复用—— 不会为每个数据项都创建一个 View,而是只创建屏幕可见数量的 View,当列表滑动时,回收不可见的 View 并重新绑定新数据,从而节省内存、提升性能。
1.2 核心工作流程(用生活场景理解)
把 ListView 想象成手机屏幕上的 “展示窗口”,数据是 “待展示的商品”,Adapter 是 “理货员”,整个流程如下:
- 窗口(ListView)告诉理货员(Adapter):我能展示多少商品(屏幕可见数量)?
- 理货员(Adapter)从仓库(数据源)取出商品(数据),装进统一的包装盒(View);
- 理货员把包装好的商品放到窗口展示;
- 用户滑动窗口(滚动列表),不可见的商品被回收,理货员给回收的包装盒重新装上新商品,再放到窗口展示;
- 仓库商品更新(数据变化),理货员通知窗口重新展示(notifyDataSetChanged ())。
1.3 ListView 与 Adapter 的绑定关系
ListView 不能直接使用数据,必须通过 Adapter 中转,常见的 Adapter 有 3 种(和上一篇 Adapter 博客呼应,但侧重 ListView 场景):
- ArrayAdapter:最简单的 Adapter,适合纯文本列表;
- SimpleAdapter:支持图文混合列表,无需自定义 Adapter;
- BaseAdapter:自定义程度最高,支持复杂布局和交互,是开发核心。
二、新手入门:3 种基础用法快速上手
这部分从最简单的场景开始,带大家快速感受 ListView 的使用逻辑,代码均提供 Java 和 Kotlin 双版本,兼顾不同开发者习惯。

2.1 场景 1:纯文本列表(ArrayAdapter)
适合展示单一文本类型的列表(如设置选项、菜单列表),一行代码即可实现,无需自定义布局。
核心步骤
- 布局文件中添加 ListView;
- 准备字符串数据源(数组或 List<String>);
- 创建 ArrayAdapter 并绑定 ListView;
- (可选)设置列表项点击事件。
完整代码
布局文件(activity_listview_text.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">
<ListView
android:id="@+id/lv_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/gray_light"
android:dividerHeight="1dp" />
</LinearLayout>
Activity 代码(Java 版)
public class TextListViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listview_text);
// 1. 找到 ListView 控件
ListView lvText = findViewById(R.id.lv_text);
// 2. 准备数据源(纯文本列表)
String[] data = {"首页", "分类", "发现", "我的", "收藏", "历史", "设置", "帮助中心"};
// 也可以用 List<String>:List<String> dataList = Arrays.asList(data);
// 3. 创建 ArrayAdapter:参数(上下文,列表项布局,数据源)
// android.R.layout.simple_list_item_1 是系统自带的纯文本布局
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1,
data
);
// 4. 给 ListView 设置 Adapter(绑定数据和视图)
lvText.setAdapter(adapter);
// 5. 列表项点击事件(可选)
lvText.setOnItemClickListener((parent, view, position, id) -> {
String selectedText = data[position];
Toast.makeText(TextListViewActivity.this, "选中:" + selectedText, Toast.LENGTH_SHORT).show();
});
}
}
Activity 代码(Kotlin 版)
class TextListViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_listview_text)
val lvText = findViewById<ListView>(R.id.lv_text)
// 数据源
val data = arrayOf("首页", "分类", "发现", "我的", "收藏", "历史", "设置", "帮助中心")
// 创建 Adapter
val adapter = ArrayAdapter(
this,
android.R.layout.simple_list_item_1,
data
)
// 绑定 Adapter
lvText.adapter = adapter
// 点击事件
lvText.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
val selectedText = data[position]
Toast.makeText(this, "选中:$selectedText", Toast.LENGTH_SHORT).show()
}
}
}
关键说明
- 系统提供了 3 种常用布局:
android.R.layout.simple_list_item_1:纯文本(常用);android.R.layout.simple_list_item_2:双文本(主文本 + 副文本);android.R.layout.simple_list_item_checked:带复选框的文本。
- ArrayAdapter 仅支持文本展示,如需图文混合,需用 SimpleAdapter 或自定义 Adapter。
2.2 场景 2:图文混合列表(SimpleAdapter)
适合展示 “图片 + 文本” 的简单列表(如联系人、商品列表缩略图),无需自定义 Adapter,通过键值对映射数据和控件。
核心步骤
- 自定义列表项布局(包含 ImageView 和 TextView);
- 准备 List<Map<String, Object>> 类型数据源(键值对形式);
- 定义 “数据键” 和 “控件 ID” 的映射关系;
- 创建 SimpleAdapter 并绑定 ListView。
完整代码
列表项布局(item_simple_listview.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:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:orientation="horizontal">
<!-- 图片(头像/缩略图) -->
<ImageView
android:id="@+id/iv_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
android:background="@drawable/shape_circle" />
<!-- 文本容器 -->
<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/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold" />
<!-- 副文本(如描述/价格) -->
<TextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textColor="@color/gray" />
</LinearLayout>
</LinearLayout>
Activity 代码(Kotlin 版)
class SimpleListViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_listview)
val lvSimple = findViewById<ListView>(R.id.lv_simple)
// 1. 准备数据源:List<Map<String, Object>>,每个 Map 对应一个列表项
val dataList = mutableListOf<Map<String, Any>>()
// 列表项1
dataList.add(
mapOf(
"icon" to R.drawable.ic_wechat, // 图片资源ID(键:icon)
"title" to "微信", // 主文本(键:title)
"desc" to "100+ 条未读消息" // 副文本(键:desc)
)
)
// 列表项2
dataList.add(
mapOf(
"icon" to R.drawable.ic_qq,
"title" to "QQ",
"desc" to "56 条未读消息"
)
)
// 列表项3
dataList.add(
mapOf(
"icon" to R.drawable.ic_alipay,
"title" to "支付宝",
"desc" to "余额:1234.56 元"
)
)
// 2. 定义数据键(与 Map 中的键对应)
val from = arrayOf("icon", "title", "desc")
// 3. 定义控件ID(与 from 数组顺序一一对应)
val to = intArrayOf(R.id.iv_icon, R.id.tv_title, R.id.tv_desc)
// 4. 创建 SimpleAdapter
val adapter = SimpleAdapter(
this,
dataList, // 数据源
R.layout.item_simple_listview, // 自定义列表项布局
from, // 数据键数组
to // 控件ID数组
)
// 5. 绑定 Adapter
lvSimple.adapter = adapter
// 6. 点击事件
lvSimple.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
val item = dataList[position]
val title = item["title"] as String
Toast.makeText(this, "打开:$title", Toast.LENGTH_SHORT).show()
}
}
}
Activity 布局(activity_simple_listview.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">
<ListView
android:id="@+id/lv_simple"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/gray_light"
android:dividerHeight="1dp" />
</LinearLayout>
关键说明
- SimpleAdapter 支持多控件映射,但仅适用于静态布局(无需动态修改控件样式);
- ImageView 仅支持本地资源(drawable/mipmap),若需加载网络图片,需自定义 Adapter 配合 Glide/Picasso。
2.3 场景 3:自定义布局 + 交互(BaseAdapter)
实际开发中,列表项往往包含复杂布局(如按钮、进度条)或动态交互(如点赞、收藏),这时必须自定义 BaseAdapter—— 这是 ListView 开发的核心技能,也是新手的重点学习内容。
BaseAdapter 核心方法
自定义 BaseAdapter 需重写 4 个抽象方法,每个方法的作用如下:
| 方法名 | 作用 | 核心注意点 |
|---|---|---|
getCount() | 返回列表项数量 | 直接返回数据源长度 |
getItem(position: Int) | 返回指定位置的数据源对象 | 用于获取当前项数据 |
getItemId(position: Int) | 返回列表项 ID | 通常返回 position 即可 |
getView(position: Int, convertView: View?, parent: ViewGroup) | 创建 / 复用视图并绑定数据 | 性能优化的核心的地方,需处理 convertView 复用和 ViewHolder 缓存 |
实战:带点赞功能的新闻列表
步骤 1:定义数据模型(NewsModel)
// 新闻数据模型:存储每条新闻的信息
data class NewsModel(
val title: String, // 新闻标题
val source: String, // 新闻来源(如“人民日报”)
val time: String, // 发布时间
val likeCount: Int, // 点赞数
var isLiked: Boolean // 是否已点赞(可变状态)
)
步骤 2:列表项布局(item_news_listview.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:padding="16dp"
android:orientation="vertical"
android:background="?attr/selectableItemBackground">
<!-- 新闻标题 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:maxLines="2"
android:ellipsize="end" />
<!-- 来源和时间 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/gray" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textSize="14sp"
android:textColor="@color/gray" />
</LinearLayout>
<!-- 点赞区域 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_like"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_like_normal" />
<TextView
android:id="@+id/tv_like_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="14sp"
android:textColor="@color/gray" />
</LinearLayout>
</LinearLayout>
步骤 3:自定义 BaseAdapter(核心)
class NewsAdapter(
private val context: Context,
private val dataList: MutableList<NewsModel>
) : BaseAdapter() {
// 1. 返回列表项数量(数据源长度)
override fun getCount(): Int = dataList.size
// 2. 返回指定位置的数据源对象
override fun getItem(position: Int): NewsModel = dataList[position]
// 3. 返回列表项ID(直接用position即可)
override fun getItemId(position: Int): Long = position.toLong()
// 4. 创建/复用视图并绑定数据(性能优化核心)
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
// ViewHolder:缓存控件实例,避免重复findViewById(关键优化)
val holder: ViewHolder
val view: View
if (convertView == null) {
// 第一次创建视图:加载布局、初始化控件、绑定ViewHolder
view = LayoutInflater.from(context).inflate(R.layout.item_news_listview, parent, false)
holder = ViewHolder()
// 绑定控件(一次查找,多次复用)
holder.tvTitle = view.findViewById(R.id.tv_title)
holder.tvSource = view.findViewById(R.id.tv_source)
holder.tvTime = view.findViewById(R.id.tv_time)
holder.ivLike = view.findViewById(R.id.iv_like)
holder.tvLikeCount = view.findViewById(R.id.tv_like_count)
// 将ViewHolder存储到View的tag中,方便复用
view.tag = holder
} else {
// 复用已有视图:直接从View的tag中获取ViewHolder
view = convertView
holder = view.tag as ViewHolder
}
// 绑定数据到控件(每次复用都要重新绑定,避免数据错乱)
val news = getItem(position)
holder.tvTitle?.text = news.title
holder.tvSource?.text = news.source
holder.tvTime?.text = news.time
holder.tvLikeCount?.text = news.likeCount.toString()
// 设置点赞状态(关键:每次绑定都要明确状态,避免复用导致的状态错乱)
if (news.isLiked) {
holder.ivLike?.setImageResource(R.drawable.ic_like_selected)
holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.red))
} else {
holder.ivLike?.setImageResource(R.drawable.ic_like_normal)
holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.gray))
}
// 点赞点击事件(动态修改状态并刷新)
holder.ivLike?.setOnClickListener {
// 修改数据源中的状态(状态必须存在数据源,不能存在View中)
news.isLiked = !news.isLiked
val newLikeCount = if (news.isLiked) news.likeCount + 1 else news.likeCount - 1
news.likeCount = newLikeCount
// 通知Adapter数据变化,更新当前项视图
notifyDataSetChanged()
}
return view
}
// 静态内部类:ViewHolder,用于缓存控件
private class ViewHolder {
var tvTitle: TextView? = null
var tvSource: TextView? = null
var tvTime: TextView? = null
var ivLike: ImageView? = null
var tvLikeCount: TextView? = null
}
}
步骤 4:Activity 中使用 Adapter
class NewsListViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_news_listview)
val lvNews = findViewById<ListView>(R.id.lv_news)
// 1. 初始化数据源
val dataList = mutableListOf<NewsModel>().apply {
add(NewsModel("Android ListView 性能优化实战", "技术干货君", "2小时前", 128, false))
add(NewsModel("ListView 与 RecyclerView 核心差异分析", "Android 开发笔记", "5小时前", 89, true))
add(NewsModel("新手必看:ListView 常见问题避坑指南", "编程小助手", "1天前", 215, false))
add(NewsModel("ListView 自定义 Adapter 完整教程", "移动开发周刊", "2天前", 342, false))
add(NewsModel("如何让 ListView 滑动流畅度提升50%", "架构师之路", "3天前", 567, true))
}
// 2. 创建自定义 Adapter
val adapter = NewsAdapter(this, dataList)
// 3. 绑定 Adapter 到 ListView
lvNews.adapter = adapter
// 4. 列表项点击事件(跳转到详情页)
lvNews.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
val news = dataList[position]
Toast.makeText(this, "查看详情:${news.title}", Toast.LENGTH_SHORT).show()
}
}
}
Activity 布局(activity_news_listview.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">
<ListView
android:id="@+id/lv_news"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/gray_light"
android:dividerHeight="1dp"
android:overScrollMode="never" />
</LinearLayout>
核心优化点:ViewHolder 模式
这是 ListView 性能优化的基石,必须理解透彻:
- 没有 ViewHolder 时:每次调用
getView()都会执行findViewById(),而findViewById()是耗时操作(需要遍历视图树),滑动列表时会频繁调用,导致卡顿; - 有 ViewHolder 时:控件实例被缓存到
ViewHolder中,convertView复用时直接从View.tag中取出,无需重复查找,滑动流畅度大幅提升。
三、进阶用法:解锁 ListView 更多实用功能
基础用法只能满足简单需求,实际开发中还会遇到 “数据更新”“长按事件”“多类型布局” 等场景,这部分带你解锁 ListView 的进阶技能。

3.1 数据更新:正确刷新列表的 3 种方式
列表数据变化后(如新增、删除、修改),需要通知 Adapter 刷新视图,常见的 3 种方式如下:
方式 1:全局刷新(notifyDataSetChanged ())
最常用的方式,会刷新整个列表的所有视图,适合数据变化较多的场景:
// 新增数据
dataList.add(NewsModel("新增新闻标题", "测试来源", "刚刚", 0, false))
// 通知 Adapter 刷新
adapter.notifyDataSetChanged()
方式 2:局部刷新(notifyItemChanged () 等)
API 11+ 提供的局部刷新方法,只刷新指定位置的视图,性能更优:
// 刷新单个位置(第2项)
adapter.notifyItemChanged(1)
// 刷新指定范围(从第0项开始,刷新3项)
adapter.notifyItemRangeChanged(0, 3)
// 新增数据后刷新新增位置
val newPosition = dataList.size
dataList.add(newItem)
adapter.notifyItemInserted(newPosition)
// 删除数据后刷新删除位置
dataList.removeAt(2)
adapter.notifyItemRemoved(2)
方式 3:替换数据源刷新
适合完全替换列表数据的场景(如下拉刷新):
// 替换整个数据源
dataList.clear()
dataList.addAll(newDataList)
// 全局刷新
adapter.notifyDataSetChanged()
关键注意事项
- 数据更新必须在主线程执行,否则会报错;
- 局部刷新方法(如
notifyItemChanged())需要 Adapter 继承BaseAdapter或ArrayAdapter(需 API 11+); - 刷新前必须确保数据源已修改,否则刷新无效。
3.2 交互事件:点击、长按、多选
ListView 提供了丰富的交互事件回调,满足日常开发需求:
(1)列表项点击事件(OnItemClickListener)
基础用法中已演示,用于响应列表项的点击操作:
lvNews.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
val news = dataList[position]
// 跳转到详情页
val intent = Intent(this, NewsDetailActivity::class.java)
intent.putExtra("news_title", news.title)
startActivity(intent)
}
(2)列表项长按事件(OnItemLongClickListener)
用于响应长按操作(如弹出菜单、删除 item):
lvNews.onItemLongClickListener = AdapterView.OnItemLongClickListener { parent, view, position, id ->
// 弹出删除菜单
AlertDialog.Builder(this)
.setTitle("提示")
.setMessage("确定要删除这条新闻吗?")
.setPositiveButton("删除") { _, _ ->
// 删除数据源中的数据
dataList.removeAt(position)
// 刷新列表
adapter.notifyDataSetChanged()
}
.setNegativeButton("取消", null)
.show()
// 返回true表示消费事件,避免同时触发点击事件
true
}
(3)多选功能(ChoiceMode)
通过设置 choiceMode 实现多选或单选功能,适合批量操作(如批量删除):
// 1. 在布局中设置 choiceMode
<ListView
android:id="@+id/lv_choice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="multipleChoice" />
// 2. 代码中获取选中的项
val selectedPositions = mutableListOf<Int>()
// 遍历所有项,判断是否选中
for (i in 0 until adapter.count) {
if (lvChoice.isItemChecked(i)) {
selectedPositions.add(i)
}
}
// 批量删除选中项(注意:倒序删除,避免索引错乱)
for (i in selectedPositions.reversed()) {
dataList.removeAt(i)
}
adapter.notifyDataSetChanged()
(4)列表项内部控件点击事件
如列表项中的按钮、图片,需要在 getView() 中给控件设置点击事件(如之前点赞功能的实现):
// 在 getView() 中给按钮设置点击事件
holder.btnDelete?.setOnClickListener {
val news = getItem(position)
Toast.makeText(context, "删除:${news.title}", Toast.LENGTH_SHORT).show()
dataList.removeAt(position)
notifyDataSetChanged()
}
3.3 多类型布局:一个列表展示不同样式
实际开发中,可能需要在一个列表中展示多种布局(如首页的 Banner 轮播 + 新闻列表 + 推荐卡片),这时需要通过 getItemViewType() 和 getViewTypeCount() 实现。
实战:包含 Banner 和新闻的混合列表
步骤 1:定义多类型数据模型(密封类)
// 密封类:统一管理多类型数据
sealed class MixedItem {
// Banner 类型:包含轮播图图片地址
data class BannerItem(val imageUrls: List<String>) : MixedItem()
// 新闻类型:复用之前的 NewsModel
data class NewsItem(val news: NewsModel) : MixedItem()
}
步骤 2:定义两种布局
- Banner 布局(item_banner.xml):
<ImageView
android:id="@+id/iv_banner"
android:layout_width="match_parent"
android:layout_height="180dp"
android:scaleType="centerCrop"
android:background="@color/gray_light" />
- 新闻布局:复用之前的
item_news_listview.xml
步骤 3:自定义多类型 BaseAdapter
class MixedAdapter(
private val context: Context,
private val dataList: MutableList<MixedItem>
) : BaseAdapter() {
// 定义布局类型常量
companion object {
private const val TYPE_BANNER = 0 // Banner 类型
private const val TYPE_NEWS = 1 // 新闻类型
}
// 1. 返回布局类型数量
override fun getViewTypeCount(): Int = 2
// 2. 返回当前位置的布局类型
override fun getItemViewType(position: Int): Int {
return when (dataList[position]) {
is MixedItem.BannerItem -> TYPE_BANNER
is MixedItem.NewsItem -> TYPE_NEWS
}
}
// 3. 返回列表项数量
override fun getCount(): Int = dataList.size
// 4. 返回当前项数据
override fun getItem(position: Int): MixedItem = dataList[position]
// 5. 返回列表项ID
override fun getItemId(position: Int): Long = position.toLong()
// 6. 创建/复用视图(根据布局类型处理)
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val viewType = getItemViewType(position)
var view: View? = convertView
var bannerHolder: BannerViewHolder? = null
var newsHolder: NewsViewHolder? = null
when (viewType) {
TYPE_BANNER -> {
if (view == null) {
// 加载 Banner 布局
view = LayoutInflater.from(context).inflate(R.layout.item_banner, parent, false)
bannerHolder = BannerViewHolder()
bannerHolder.ivBanner = view.findViewById(R.id.iv_banner)
view.tag = bannerHolder
} else {
bannerHolder = view.tag as BannerViewHolder
}
// 绑定 Banner 数据
val bannerItem = getItem(position) as MixedItem.BannerItem
// 用 Glide 加载第一张轮播图(实际开发中需用轮播图控件)
Glide.with(context)
.load(bannerItem.imageUrls[0])
.placeholder(R.color.gray_light)
.into(bannerHolder.ivBanner!!)
}
TYPE_NEWS -> {
if (view == null) {
// 加载新闻布局
view = LayoutInflater.from(context).inflate(R.layout.item_news_listview, parent, false)
newsHolder = NewsViewHolder()
// 绑定新闻布局控件
newsHolder.tvTitle = view.findViewById(R.id.tv_title)
newsHolder.tvSource = view.findViewById(R.id.tv_source)
newsHolder.tvTime = view.findViewById(R.id.tv_time)
newsHolder.ivLike = view.findViewById(R.id.iv_like)
newsHolder.tvLikeCount = view.findViewById(R.id.tv_like_count)
view.tag = newsHolder
} else {
newsHolder = view.tag as NewsViewHolder
}
// 绑定新闻数据
val newsItem = getItem(position) as MixedItem.NewsItem
val news = newsItem.news
newsHolder.tvTitle?.text = news.title
newsHolder.tvSource?.text = news.source
newsHolder.tvTime?.text = news.time
newsHolder.tvLikeCount?.text = news.likeCount.toString()
// 点赞状态
if (news.isLiked) {
newsHolder.ivLike?.setImageResource(R.drawable.ic_like_selected)
newsHolder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.red))
} else {
newsHolder.ivLike?.setImageResource(R.drawable.ic_like_normal)
newsHolder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.gray))
}
}
}
return view!!
}
// Banner 布局的 ViewHolder
private class BannerViewHolder {
var ivBanner: ImageView? = null
}
// 新闻布局的 ViewHolder
private class NewsViewHolder {
var tvTitle: TextView? = null
var tvSource: TextView? = null
var tvTime: TextView? = null
var ivLike: ImageView? = null
var tvLikeCount: TextView? = null
}
}
步骤 4:Activity 中使用
class MixedListViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mixed_listview)
val lvMixed = findViewById<ListView>(R.id.lv_mixed)
// 初始化多类型数据源
val dataList = mutableListOf<MixedItem>().apply {
// 添加 Banner 项
add(MixedItem.BannerItem(
listOf(
"https://picsum.photos/800/400?1",
"https://picsum.photos/800/400?2",
"https://picsum.photos/800/400?3"
)
))
// 添加新闻项
add(MixedItem.NewsItem(NewsModel("ListView 多类型布局实战", "技术君", "1小时前", 66, false)))
add(MixedItem.NewsItem(NewsModel("Android 混合列表开发技巧", "开发笔记", "3小时前", 99, true)))
}
// 创建 Adapter 并绑定
val adapter = MixedAdapter(this, dataList)
lvMixed.adapter = adapter
}
}
四、性能优化:让 ListView 滑动如丝般顺滑
很多开发者抱怨 ListView 卡顿,其实大部分卡顿都是因为没有做好优化。掌握以下 5 个核心优化技巧,能让 ListView 的滑动流畅度提升 50% 以上。
4.1 必做优化:ViewHolder 模式(已讲过,再强调)
这是最基础也是最重要的优化,核心是缓存控件实例,避免重复 findViewById ()。
- 没有 ViewHolder 时:滑动列表会频繁执行
findViewById(),遍历视图树耗时,导致卡顿; - 有 ViewHolder 时:控件实例只查找一次,复用视图时直接取出,性能大幅提升。
4.2 关键优化:复用 convertView
getView() 方法的 convertView 参数是系统回收的不可见视图,必须复用,不能每次都 inflate 新布局:
错误写法(严禁)
// 每次都加载新布局,不复用 convertView,内存暴涨+卡顿
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = LayoutInflater.from(context).inflate(R.layout.item_news, parent, false)
// ... 绑定数据
return view
}
正确写法(必须)
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View
if (convertView == null) {
view = LayoutInflater.from(context).inflate(R.layout.item_news, parent, false)
// ... 初始化 ViewHolder
} else {
view = convertView
// ... 取出 ViewHolder
}
// ... 绑定数据
return view
}
4.3 进阶优化:图片加载优化
列表中的图片是性能消耗的重灾区,优化不当会导致 OOM(内存溢出)或滑动卡顿,核心优化点如下:
(1)使用图片加载库(Glide/Picasso)
自带图片压缩、缓存、延迟加载功能,避免手动处理图片导致的问题:
Glide.with(context)
.load(imageUrl) // 图片地址(网络/本地)
.placeholder(R.drawable.ic_placeholder) // 占位图
.error(R.drawable.ic_error) // 加载失败图
.override(300, 200) // 压缩图片尺寸(适应列表项大小)
.centerCrop() // 缩放模式
.diskCacheStrategy(DiskCacheStrategy.ALL) // 缓存策略(内存+磁盘)
.into(imageView)
(2)避免加载过大图片
根据列表项的实际尺寸加载对应分辨率的图片,比如列表项图片尺寸是 300x200,就不要加载 1080x1920 的原图。
(3)列表滑动时暂停图片加载
监听 ListView 的滑动状态,滑动时暂停加载,停止滑动后再加载,减少滑动时的性能消耗:
lvNews.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {
when (scrollState) {
// 滑动时暂停加载
AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL -> {
Glide.with(context).pauseRequests()
}
// 停止滑动后恢复加载
AbsListView.OnScrollListener.SCROLL_STATE_IDLE -> {
Glide.with(context).resumeRequests()
}
}
}
override fun onScroll(view: AbsListView?, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {}
})
4.4 高级优化:分批加载数据(分页加载)
当列表数据量较大(如几百条、几千条)时,一次性加载所有数据会导致初始化缓慢、内存占用过高。分批加载(分页)是解决这个问题的关键:
核心逻辑
- 首次加载前 N 条数据(如 20 条);
- 监听 ListView 滑动到底部,加载下一页数据;
- 用
notifyItemRangeInserted()刷新新增数据(避免全局刷新)。
实战代码
class PagingAdapter(
private val context: Context,
private val dataList: MutableList<NewsModel>
) : BaseAdapter() {
// 标记是否正在加载(避免重复请求)
var isLoading = false
// 添加分页数据
fun addPagingData(newData: List<NewsModel>) {
val startPosition = dataList.size
dataList.addAll(newData)
// 只刷新新增的项,性能更优
notifyItemRangeInserted(startPosition, newData.size)
isLoading = false
}
// ... 其他方法(getCount、getItem、getView 等,和之前一致)
}
// Activity 中实现分页逻辑
class PagingListViewActivity : AppCompatActivity() {
private lateinit var lvPaging: ListView
private lateinit var adapter: PagingAdapter
private val dataList = mutableListOf<NewsModel>()
private var currentPage = 1 // 当前页码
private val pageSize = 20 // 每页数据量
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_paging_listview)
lvPaging = findViewById(R.id.lv_paging)
adapter = PagingAdapter(this, dataList)
lvPaging.adapter = adapter
// 首次加载第一页数据
loadData(currentPage)
// 监听滑动到底部,加载下一页
lvPaging.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {}
override fun onScroll(
view: AbsListView?,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
// 滑动到底部且不在加载中,加载下一页
if (!adapter.isLoading
&& firstVisibleItem + visibleItemCount >= totalItemCount - 5) {
currentPage++
loadData(currentPage)
}
}
})
}
// 模拟网络请求加载数据
private fun loadData(page: Int) {
adapter.isLoading = true
// 显示加载中提示(可在列表底部添加加载视图)
showLoadingFooter(true)
// 协程模拟网络请求(实际开发中替换为真实接口请求)
lifecycleScope.launch {
delay(1000) // 模拟网络延迟
// 模拟分页数据
val newData = mutableListOf<NewsModel>()
for (i in 0 until pageSize) {
newData.add(
NewsModel(
title = "第 $page 页 - 新闻标题 ${i+1}",
source = "分页加载测试",
time = "${page}小时前",
likeCount = Random.nextInt(0, 500),
isLiked = false
)
)
}
// 添加数据到 Adapter
adapter.addPagingData(newData)
// 隐藏加载中提示
showLoadingFooter(false)
}
}
// 显示/隐藏加载中底部视图(简化实现)
private fun showLoadingFooter(show: Boolean) {
if (show) {
Toast.makeText(this, "加载中...", Toast.LENGTH_SHORT).show()
} else {
// 实际开发中可移除底部加载视图
}
}
}
4.5 细节优化:减少布局层级 + 避免过度绘制
- 减少布局层级:用 ConstraintLayout 替代多层 LinearLayout,避免嵌套过深(如列表项布局用 ConstraintLayout 替代 LinearLayout 嵌套);
- 移除不必要的背景:如果父布局和子布局的背景重叠,移除子布局的背景,避免过度绘制;
- 使用 merge 标签:列表项布局的根标签用
merge,复用父布局的布局参数,减少一层布局层级:
<!-- 优化前:LinearLayout 作为根布局,增加层级 -->
<LinearLayout ...>
<!-- 控件... -->
</LinearLayout>
<!-- 优化后:merge 标签,减少层级 -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 控件... -->
</merge>
- 避免在 getView () 中执行耗时操作:如网络请求、数据库查询、复杂计算,这些操作应在子线程执行,执行完成后再更新 UI。
五、避坑指南:解决 ListView 开发中最常见的 8 个问题

ListView 开发中,很多新手会遇到各种 “玄学问题”,其实都是有固定原因的。下面总结 8 个高频问题,每个问题都讲清 “原因 + 解决方案”,让你少踩坑。
5.1 问题 1:列表滑动时数据错乱(最常见)
原因
视图复用导致的状态混乱 —— 比如点赞状态、选中状态只存储在 View 中,复用视图时未重新设置状态,导致显示上一个 item 的状态。
解决方案
- 状态必须存储在数据源中(如
NewsModel中的isLiked字段),不能存储在 View 或 ViewHolder 中; - 每次调用
getView()时,无论视图是否复用,都要明确设置控件状态(如点赞图标、文本颜色)。
错误示例(严禁)
// 状态存储在 ViewHolder 中,复用后会错乱
private class ViewHolder {
var ivLike: ImageView? = null
var isLiked: Boolean = false // 错误:状态存储在 ViewHolder
}
正确示例(必须)
// 状态存储在数据源中
data class NewsModel(..., var isLiked: Boolean)
// getView() 中明确设置状态
if (news.isLiked) {
holder.ivLike?.setImageResource(R.drawable.ic_like_selected)
} else {
holder.ivLike?.setImageResource(R.drawable.ic_like_normal)
}
5.2 问题 2:notifyDataSetChanged () 不生效
原因
- 数据源未真正修改(如用
val定义的列表,重新赋值后未通知 Adapter); - Adapter 引用的数据源对象未更新(如修改了列表元素但未调用
notify); - 在子线程中调用
notifyDataSetChanged()。
解决方案
- 用可变集合(
MutableList)存储数据,修改后调用notify方法; - 如果替换整个数据源,确保 Adapter 引用的是新的列表对象,再调用
notify; - 必须在主线程调用
notify方法。
正确示例
// 1. 用 MutableList 存储数据
private val dataList = mutableListOf<NewsModel>()
// 2. 修改数据后调用 notify
dataList.add(newItem)
adapter.notifyDataSetChanged()
// 3. 子线程中更新数据(需切换到主线程)
lifecycleScope.launch(Dispatchers.IO) {
// 子线程获取数据
val newData = fetchData()
// 切换到主线程更新 UI
withContext(Dispatchers.Main) {
dataList.addAll(newData)
adapter.notifyDataSetChanged()
}
}
5.3 问题 3:列表项内部控件点击事件失效
原因
- 控件设置了
android:clickable="false"; - 列表项的根布局设置了
onClick事件,与内部控件点击事件冲突; - 控件被其他视图遮挡。
解决方案
- 确保内部控件的
clickable属性为true(默认是true,无需手动设置); - 内部控件点击事件优先级高于列表项点击事件,无需担心冲突;
- 检查布局是否有遮挡(如父布局的
padding或margin设置不当)。
5.4 问题 4:ListView 没有滑动到底部却触发了分页加载
原因
onScroll 方法中判断滑动到底部的条件不准确,导致提前触发加载。
解决方案
使用精准的判断条件:
override fun onScroll(
view: AbsListView?,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
// 条件:不在加载中 + 可见项数量 > 0 + 最后一个可见项位置 >= 总数量 - 5(预加载)
val isBottom = firstVisibleItem + visibleItemCount >= totalItemCount - 5
if (!adapter.isLoading && visibleItemCount > 0 && isBottom) {
// 加载下一页
}
}
5.5 问题 5:图片加载错位(滑动时图片乱跳)
原因
图片加载是异步操作,视图复用时,上一个图片的加载请求还未完成,导致新图片覆盖旧图片,或旧图片显示在新视图上。
解决方案
- 使用图片加载库的
clear()方法,加载新图片前清除旧图片; - 给 ImageView 设置
tag,确保图片加载完成后绑定到正确的视图上。
代码示例
// 给 ImageView 设置 tag(图片地址)
imageView.tag = imageUrl
// 加载图片前清除旧图片
Glide.with(context)
.load(imageUrl)
.placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_error)
.centerCrop()
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
// 验证 tag 是否匹配,避免错位
if (imageView.tag == imageUrl) {
imageView.setImageDrawable(resource)
}
}
override fun onLoadCleared(placeholder: Drawable?) {
imageView.setImageDrawable(placeholder)
}
})
5.6 问题 6:ListView 滑动卡顿,日志显示 “跳过多少帧”
原因
- 没有使用 ViewHolder 模式,频繁
findViewById(); - 图片加载未优化,尺寸过大或未缓存;
getView()中执行了耗时操作(如复杂计算、IO 操作);- 布局层级过深,过度绘制严重。
解决方案
- 严格使用 ViewHolder 模式;
- 按前面的图片优化技巧处理图片;
- 耗时操作移到子线程执行;
- 优化布局层级,减少过度绘制(可通过 “开发者选项→调试 GPU 过度绘制” 查看)。
5.7 问题 7:ListView 不显示分割线
原因
- 未设置
divider或dividerHeight; divider设置为透明色;- 列表项布局的背景覆盖了分割线。
解决方案
- 在布局中明确设置分割线和高度:
<ListView
android:id="@+id/lv_news"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/gray_light" // 分割线颜色
android:dividerHeight="1dp" /> // 分割线高度
- 确保列表项布局的背景不会覆盖分割线(如避免设置
layout_marginBottom为负数)。
5.8 问题 8:ListView 数据更新后,滚动位置重置到顶部
原因
调用 notifyDataSetChanged() 后,ListView 会重新计算布局,导致滚动位置重置。
解决方案
更新数据前保存滚动位置,更新后恢复:
// 保存滚动位置
val firstVisiblePosition = lvNews.firstVisiblePosition
val top = (lvNews.getChildAt(0)?.top ?: 0) - lvNews.paddingTop
// 更新数据
dataList.clear()
dataList.addAll(newData)
adapter.notifyDataSetChanged()
// 恢复滚动位置
lvNews.setSelectionFromTop(firstVisiblePosition, top)
六、综合实战:完整的网络数据列表(含下拉刷新 + 分页)

整合前面的知识点,实现一个 “网络请求 + 下拉刷新 + 上拉分页 + 点赞功能 + 局部刷新” 的完整列表,模拟真实开发场景。
6.1 案例需求
- 调用公开 API 获取新闻列表数据;
- 支持下拉刷新(刷新第一页数据);
- 支持上拉加载更多(分页加载);
- 列表项包含标题、来源、时间、点赞功能;
- 点赞后局部刷新点赞状态和数量,不刷新整个列表。
6.2 核心依赖
// 网络请求
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 图片加载
implementation 'com.github.bumptech.glide:glide:4.16.0'
kapt 'com.github.bumptech.glide:compiler:4.16.0'
// 下拉刷新
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
6.3 核心代码
步骤 1:网络请求相关(API 接口、数据模型)
// 新闻数据模型(适配 API 返回格式)
data class NewsResponse(
val code: Int,
val msg: String,
val data: List<NewsModel>
)
// 新闻实体类(复用之前的 NewsModel)
data class NewsModel(
val id: Long,
val title: String,
val source: String,
val time: String,
val likeCount: Int,
var isLiked: Boolean
)
// Retrofit API 接口
interface NewsApiService {
@GET("news/list")
suspend fun getNewsList(
@Query("page") page: Int,
@Query("size") size: Int
): Response<NewsResponse>
}
// 网络请求工具类
object RetrofitClient {
private const val BASE_URL = "https://api.example.com/" // 替换为真实 API 地址
val apiService: NewsApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NewsApiService::class.java)
}
}
步骤 2:自定义 Adapter(支持局部刷新)
class CompleteNewsAdapter(
private val context: Context,
private val dataList: MutableList<NewsModel>,
private val onLikeClick: (Long, Boolean) -> Unit // 点赞回调(新闻ID,是否点赞)
) : BaseAdapter() {
// ViewHolder 缓存控件
private class ViewHolder {
var tvTitle: TextView? = null
var tvSource: TextView? = null
var tvTime: TextView? = null
var ivLike: ImageView? = null
var tvLikeCount: TextView? = null
}
override fun getCount(): Int = dataList.size
override fun getItem(position: Int): NewsModel = dataList[position]
override fun getItemId(position: Int): Long = dataList[position].id
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val holder: ViewHolder
val view: View
if (convertView == null) {
view = LayoutInflater.from(context).inflate(R.layout.item_news_listview, parent, false)
holder = ViewHolder()
holder.tvTitle = view.findViewById(R.id.tv_title)
holder.tvSource = view.findViewById(R.id.tv_source)
holder.tvTime = view.findViewById(R.id.tv_time)
holder.ivLike = view.findViewById(R.id.iv_like)
holder.tvLikeCount = view.findViewById(R.id.tv_like_count)
view.tag = holder
} else {
view = convertView
holder = view.tag as ViewHolder
}
// 绑定数据
val news = getItem(position)
holder.tvTitle?.text = news.title
holder.tvSource?.text = news.source
holder.tvTime?.text = news.time
holder.tvLikeCount?.text = news.likeCount.toString()
// 点赞状态
if (news.isLiked) {
holder.ivLike?.setImageResource(R.drawable.ic_like_selected)
holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.red))
} else {
holder.ivLike?.setImageResource(R.drawable.ic_like_normal)
holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.gray))
}
// 点赞点击事件
holder.ivLike?.setOnClickListener {
val newLiked = !news.isLiked
val newLikeCount = if (newLiked) news.likeCount + 1 else news.likeCount - 1
// 更新数据源
news.isLiked = newLiked
news.likeCount = newLikeCount
// 局部刷新当前项(只刷新点赞相关控件,性能最优)
notifyItemChanged(position)
// 回调给 Activity 发送点赞网络请求
onLikeClick(news.id, newLiked)
}
return view
}
// 添加分页数据
fun addPagingData(newData: List<NewsModel>) {
val startPos = dataList.size
dataList.addAll(newData)
notifyItemRangeInserted(startPos, newData.size)
}
// 刷新第一页数据(下拉刷新)
fun refreshData(newData: List<NewsModel>) {
dataList.clear()
dataList.addAll(newData)
notifyDataSetChanged()
}
}
步骤 3:Activity 实现(下拉刷新 + 分页加载)
class CompleteListViewActivity : AppCompatActivity() {
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private lateinit var lvNews: ListView
private lateinit var adapter: CompleteNewsAdapter
private val dataList = mutableListOf<NewsModel>()
private var currentPage = 1
private val pageSize = 20
private var isLoading = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_complete_listview)
// 初始化控件
swipeRefreshLayout = findViewById(R.id.srl_refresh)
lvNews = findViewById(R.id.lv_news)
// 初始化 Adapter
adapter = CompleteNewsAdapter(this, dataList) { newsId, isLiked ->
// 点赞回调:发送网络请求(实际开发中实现)
Toast.makeText(this, "点赞状态:$isLiked", Toast.LENGTH_SHORT).show()
}
lvNews.adapter = adapter
// 下拉刷新配置
swipeRefreshLayout.setColorSchemeResources(R.color.blue)
swipeRefreshLayout.setOnRefreshListener {
// 刷新第一页
currentPage = 1
loadNewsData(currentPage, isRefresh = true)
}
// 上拉加载更多
lvNews.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {}
override fun onScroll(
view: AbsListView?,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
val isBottom = firstVisibleItem + visibleItemCount >= totalItemCount - 5
if (!isLoading && !swipeRefreshLayout.isRefreshing && visibleItemCount > 0 && isBottom) {
currentPage++
loadNewsData(currentPage, isRefresh = false)
}
}
})
// 首次加载数据
loadNewsData(currentPage, isRefresh = false)
}
// 加载新闻数据
private fun loadNewsData(page: Int, isRefresh: Boolean) {
isLoading = true
if (!isRefresh) {
Toast.makeText(this, "加载中...", Toast.LENGTH_SHORT).show()
}
// 协程发起网络请求
lifecycleScope.launch(Dispatchers.IO) {
try {
val response = RetrofitClient.apiService.getNewsList(page, pageSize)
if (response.isSuccessful) {
val newsResponse = response.body()
if (newsResponse?.code == 200) {
val newData = newsResponse.data ?: emptyList()
// 切换到主线程更新 UI
withContext(Dispatchers.Main) {
if (isRefresh) {
// 下拉刷新:替换数据
adapter.refreshData(newData)
swipeRefreshLayout.isRefreshing = false
} else {
// 上拉加载:追加数据
adapter.addPagingData(newData)
}
}
} else {
withContext(Dispatchers.Main) {
Toast.makeText(this@CompleteListViewActivity, "加载失败:${newsResponse?.msg}", Toast.LENGTH_SHORT).show()
swipeRefreshLayout.isRefreshing = false
}
}
} else {
withContext(Dispatchers.Main) {
Toast.makeText(this@CompleteListViewActivity, "请求失败", Toast.LENGTH_SHORT).show()
swipeRefreshLayout.isRefreshing = false
}
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(this@CompleteListViewActivity, "网络错误", Toast.LENGTH_SHORT).show()
swipeRefreshLayout.isRefreshing = false
}
} finally {
isLoading = false
}
}
}
}
步骤 4:Activity 布局(含下拉刷新)
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/srl_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lv_news"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/gray_light"
android:dividerHeight="1dp"
android:overScrollMode="never" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
七、总结与拓展
7.1 核心知识点回顾
- ListView 是 Android 经典列表控件,核心优势是视图复用;
- 基础用法:ArrayAdapter(纯文本)、SimpleAdapter(简单图文)、BaseAdapter(自定义);
- 进阶用法:多类型布局、数据更新、交互事件(点击 / 长按 / 多选);
- 性能优化:ViewHolder 模式、convertView 复用、图片优化、分页加载、布局优化;
- 避坑重点:数据错乱(状态存数据源)、刷新不生效(主线程 + 可变集合)、图片错位(tag + 清除旧请求)。
7.2 ListView 与 RecyclerView 对比
很多开发者会纠结选 ListView 还是 RecyclerView,这里做一个简洁对比,帮你快速选择:
| 特性 | ListView | RecyclerView |
|---|---|---|
| 视图复用 | 支持(需手动处理 convertView) | 支持(强制 ViewHolder) |
| 布局支持 | 仅线性布局(通过 ListAdapter) | 线性、网格、瀑布流(LayoutManager) |
| 动画支持 | 基本不支持 | 自带 Item 动画,支持自定义 |
| 多类型布局 | 支持(需重写 getItemViewType) | 支持(更简洁) |
| 性能 | 一般(需手动优化) | 更优(分级缓存 + 强制优化) |
| 扩展性 | 较差 | 极强(模块化设计) |
| 适用场景 | 简单列表、legacy 项目 | 复杂列表、新项目、性能要求高 |
7.3 拓展学习方向
- 自定义 ListView 分割线(ItemDecoration 类似功能);
- ListView 下拉刷新的自定义样式;
- 列表项侧滑删除功能(需自定义触摸事件);
- 结合 ViewModel、LiveData 实现数据与 UI 分离;
- 学习 RecyclerView 的高级用法(作为 ListView 的升级替代)。
7.4 实战建议
- 新手从 BaseAdapter 入手,理解视图复用和 ViewHolder 原理,这是列表开发的基础;
- 实际开发中,简单列表可用 ListView,复杂列表(如多类型、瀑布流)优先用 RecyclerView;
- 无论用哪种控件,性能优化的核心都是 “减少不必要的创建和查找”,比如缓存控件、复用视图、分批加载;
- 多动手写代码,尤其是性能优化和避坑部分,只有实战才能真正掌握。
这篇文章从基础到进阶,覆盖了 ListView 的全部核心知识点,所有代码均为实战原创,可直接复制运行。如果你正在开发或维护使用 ListView 的项目,这篇文章能帮你解决大部分问题。
1324

被折叠的 条评论
为什么被折叠?



