1. ListView 与 Adapter 到底谁在管数据:Android 列表入门第一课
ListView 是 Android 里最经典的滚动列表控件,它能做什么?把一堆结构相同的数据(字符串、键值对、自定义对象)按行铺开,超出屏幕后可以上下滑动。适合谁?刚学完 Activity 和基础控件、想搞懂「数据怎么变成一行行界面」的初学者。核心检索词就三个:ListView、Adapter、适配器绑定关系。
很多人第一次写列表会卡在同一个地方:明明把数据塞进了 ListView,界面却一片空白。原因几乎都是没理解 Adapter 的角色。你可以把 ListView 想成一个空货架,它只负责「摆」和「滚动」,完全不知道要摆什么。Adapter 则是理货员,它拿着你的数据源,按行生成一个个 View,再交给 ListView 去展示。没有理货员,货架永远是空的。
所以绑定关系是这条链:数据源 → Adapter → ListView.setAdapter()。数据源可以是 String[]、List、List<自定义对象>;Adapter 决定了每一行长什么样、显示哪些字段。Android 官方提供了多种 Adapter,入门阶段最常接触的是 ArrayAdapter、SimpleAdapter、BaseAdapter 这三种,它们的能力从弱到强,写法从简到繁。
我试过把这三个放在同一个项目里对比,结论很清晰:ArrayAdapter 适合纯文本单行;SimpleAdapter 适合「多字段但布局固定」的键值对;BaseAdapter 适合自定义对象、要放图片或复杂控件、还要做性能优化的场景。下面按这个顺序逐个拆,每个都给可直接复制的布局和代码,最后附运行验证步骤。
需要说明的是,本文所有代码基于 Android Studio 默认工程结构,包名用 com.example.listviewdemo 演示,你替换成自己的即可。布局文件放在 res/layout 下,图片资源放 res/drawable。跑通一个可滚动列表,从下一节开始动手。
2. 三种 Adapter 的适用场景与差异对照:ArrayAdapter、SimpleAdapter、BaseAdapter 怎么选
在动手写代码前,先把三者的定位讲清楚,这样你写的时候知道自己在干什么,而不是照抄。
ArrayAdapter 是最轻的适配器。它的数据源通常是一个数组或 List,每一项是一个对象,默认把对象的 toString() 结果塞进一个 TextView。系统自带了几种 item 布局,比如 android.R.layout.simple_list_item_1 就是「一行一个 TextView」。它的优点是三行代码就能出列表,缺点是只能显示单一文本,想加第二个控件就得自己写布局,而 ArrayAdapter 对自定义布局的支持比较别扭。
SimpleAdapter 的数据源是 List>,也就是一串键值对。你告诉它:用哪些 key 取数据、对应 item 布局里哪些控件 id。它内部帮你完成「取值 → 塞进控件」的映射。优点是布局可以自定义、多字段映射不用写循环;缺点是数据类型被限制在 Map 里,遇到自定义对象要先转成 Map,而且图片这类资源处理起来不直观。
BaseAdapter 是抽象基类,前面两个其实都继承自它。它要求你重写四个方法:getCount()、getItem()、getItemId()、getView()。getCount 告诉 ListView 一共多少行;getView 负责「给我第 position 行的 View」。因为完全自己控制,所以能放任意控件、绑定任意对象、做 convertView 复用优化。缺点是代码量最大,初学者容易在 getView 里踩坑。
用一张表对照更直观:
| 维度 | ArrayAdapter | SimpleAdapter | BaseAdapter |
|---|---|---|---|
| 数据源 | 数组 / List | List | 任意 List |
| item 布局 | 系统默认或简单自定义 | 自定义 | 完全自定义 |
| 显示字段 | 单文本 | 多字段键值对 | 任意 |
| 支持图片 | 不方便 | 不直观 | 支持 |
| 代码量 | 最少 | 中等 | 最多 |
| 适用场景 | 纯文字列表 | 固定多字段列表 | 复杂/自定义对象 |
选择逻辑可以记成一句话:能简单就别复杂。纯文字用 ArrayAdapter;两三个文本字段用 SimpleAdapter;要图片、要按钮、要性能优化,直接上 BaseAdapter。接下来三节分别给出完整可运行代码。
3. ArrayAdapter 与 SimpleAdapter 可复制配置:布局 XML 加 Java 适配器代码
先建工程,然后在 res/layout/activity_main.xml 里放一个 ListView。这个布局三个示例共用,直接复制:
<?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_id" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>3.1 ArrayAdapter 示例
数据源用字符串数组,item 用系统自带的 simple_list_item_1。MainActivity.java:
package com.example.listviewdemo; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { private ListView mListView; private String[] data = {"item_One", "item_Two", "item_Three", "item_Four", "item_Five", "item_Six", "item_Seven", "item_Eight", "item_Nine", "item_Ten"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = findViewById(R.id.lv_id); // 参数1 上下文;参数2 item 布局;参数3 数据源 ArrayAdapter<String> adapter = new ArrayAdapter<>( this, android.R.layout.simple_list_item_1, data); mListView.setAdapter(adapter); } }运行后你会看到十行文字,可以上下滑动。这就是最小可用的列表。
3.2 SimpleAdapter 示例
SimpleAdapter 需要自定义 item 布局,因为要放两个 TextView。新建 res/layout/listview_item.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="12dp"> <TextView android:id="@+id/text_one" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16sp" android:textColor="#222222" /> <TextView android:id="@+id/text_two" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="#888888" /> </LinearLayout>MainActivity 里构造 List<HashMap<String,String>>,再交给 SimpleAdapter:
package com.example.listviewdemo; import android.os.Bundle; import android.widget.ListView; import android.widget.SimpleAdapter; import androidx.appcompat.app.AppCompatActivity; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView mListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = findViewById(R.id.lv_id); List<HashMap<String, String>> data = new ArrayList<>(); for (int i = 1; i <= 18; i++) { HashMap<String, String> map = new HashMap<>(); map.put("key_one", "dataOne_" + i); map.put("key_two", "dataTwo_" + i); data.add(map); } // 参数4 是 map 的 key 数组,参数5 是对应的控件 id 数组,顺序一一对应 SimpleAdapter adapter = new SimpleAdapter( this, data, R.layout.listview_item, new String[]{"key_one", "key_two"}, new int[]{R.id.text_one, R.id.text_two}); mListView.setAdapter(adapter); } }注意参数 4 和参数 5 是严格按位置对应的:key_one 的值进 text_one,key_two 的值进 text_two。写反了数据就会串位,这是新手最常见的错误之一。
4. BaseAdapter 自定义适配器完整写法:getView 复用与运行验证步骤
BaseAdapter 是重点,也是面试和实战里用得最多的。我们做一个学生信息列表,每行显示姓名、年龄和一张头像。
先定义数据类 StudentData.java:
package com.example.listviewdemo; public class StudentData { private String name; private int age; private int photo; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getPhoto() { return photo; } public void setPhoto(int photo) { this.photo = photo; } }item 布局 listview_item.xml 换成带 ImageView 的版本:
<?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="horizontal" android:gravity="center_vertical" android:padding="12dp"> <ImageView android:id="@+id/photo_id" 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/text_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" android:textColor="#222222" /> <TextView android:id="@+id/text_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="#888888" /> </LinearLayout> </LinearLayout>自定义适配器 MyListAdapter.java,这里用 convertView 复用避免重复 inflate:
package com.example.listviewdemo; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import java.util.List; public class MyListAdapter extends BaseAdapter { private List<StudentData> mStudentDataList; private LayoutInflater inflater; public MyListAdapter(List<StudentData> list, Context context) { this.mStudentDataList = list; this.inflater = LayoutInflater.from(context); } @Override public int getCount() { return mStudentDataList == null ? 0 : mStudentDataList.size(); } @Override public Object getItem(int position) { return mStudentDataList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.listview_item, parent, false); holder = new ViewHolder(); holder.tvName = convertView.findViewById(R.id.text_name); holder.tvAge = convertView.findViewById(R.id.text_age); holder.imPhoto = convertView.findViewById(R.id.photo_id); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } StudentData data = mStudentDataList.get(position); holder.tvName.setText(data.getName()); holder.tvAge.setText("age: " + data.getAge()); holder.imPhoto.setImageResource(data.getPhoto()); return convertView; } static class ViewHolder { TextView tvName; TextView tvAge; ImageView imPhoto; } }MainActivity 里初始化数据并绑定:
package com.example.listviewdemo; import android.os.Bundle; import android.widget.ListView; import androidx.appcompat.app.AppCompatActivity; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView mListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = findViewById(R.id.lv_id); List<StudentData> list = new ArrayList<>(); for (int i = 1; i <= 18; i++) { StudentData s = new StudentData(); s.setName("lum " + i + " 号"); s.setAge(i); s.setPhoto(R.drawable.lum); // 换成你自己的图片资源名 list.add(s); } MyListAdapter adapter = new MyListAdapter(list, this); mListView.setAdapter(adapter); } }运行验证步骤:把图片放进 res/drawable 并命名为 lum.png;点击 Android Studio 的 Run;模拟器启动后应看到 18 行,每行有头像、姓名、年龄,上下滑动流畅。如果图片没显示,先检查资源名拼写和 R.drawable.lum 是否报红。
5. 本篇常见报错排查:从 NullPointerException 到列表空白逐条对照
列表跑不起来,报错往往集中在几个固定位置。下面按真实报错对照排查。
第一个高频问题是列表一片空白,日志没有崩溃。原因通常是 setAdapter 没调用,或者数据源 size 为 0。检查你的 getCount() 是否返回了正确数量,ArrayAdapter 场景检查数组是否为空。还有一种情况是 ListView 被外层布局高度压成了 0,确认 layout_height 是 match_parent 或具体值。
第二个是 NullPointerException 指向 getView 里的 findViewById。这几乎都是 convertView 为 null 时忘了 inflate,或者 ViewHolder 没 setTag 就直接 getTag。对照上一节代码,确保 inflate 的第二个参数传了 parent、第三个参数传 false,这样布局的 LayoutParams 才正确。
第三个是数据串位,滑动后某几行的文字变成别人的。这是 convertView 复用后没有重新 setText 导致的。复用的 View 带着旧数据,你必须在 getView 里对每个控件重新赋值,不能只在 convertView == null 的分支里设置。
第四个是 SimpleAdapter 显示不出内容。检查参数 4 的 key 数组和 map 里 put 的 key 是否完全一致,大小写都算;再检查参数 5 的控件 id 是否存在于 item 布局里。key 对不上,取出来就是 null,控件自然空白。
第五个是图片不显示或报 Resources$NotFoundException。确认图片确实在 res/drawable 下,文件名只能是小写字母、数字、下划线,不能有大写和横线。R.drawable.lum 要和文件名严格对应。
第六个是点击无响应。ListView 的 item 点击需要在 Adapter 的 getView 返回的根布局上可点击,如果 item 里有 Button 这类抢占焦点的控件,会吃掉点击事件。入门阶段先别在 item 里放 Button,或者给 Button 设 focusable="false"。
排查顺序建议:先看 Logcat 有没有崩溃栈,定位到具体行;没崩溃就看数据源 size;再确认 setAdapter 执行了;最后检查布局层级和控件 id。按这个顺序,九成问题能自己解决。
6. 从跑通到用顺:ListView 适配器入门后的下一步
三个示例跑通后,你已经掌握了 ListView 和 Adapter 的核心绑定关系。接下来可以做的练习:把 ArrayAdapter 的数据换成从网络或数据库读出来的 List;给 BaseAdapter 的 item 加点击回调,用接口把 position 传回 Activity;把 convertView 复用逻辑再检查一遍,理解为什么它能减少内存抖动。
如果你在配置环境或调试适配器时需要查文档、验证模型输出,可以用 TaoToken 的模型对话快速问一些 API 用法和报错含义,接入文档在 https://taotoken.net/api 有说明,API Key 在 https://taotoken.net/api-keys 获取。长期写 Android 项目、需要稳定调用编码模型的,可以看 Coding Plan:https://taotoken.net/coding-plan 。这些只是辅助工具,核心还是把 getView 的复用逻辑亲手写几遍。
最后留一个实用技巧:调试 BaseAdapter 时,在 getView 第一行打 Log.d("Adapter", "position=" + position + " convertView=" + (convertView == null)),滑动列表观察日志,你能直观看到哪些行复用了旧 View。这个习惯能帮你彻底搞懂 ListView 的回收机制,也为以后学 RecyclerView 打好底子。