
1. 先搞清楚 Android 列表适配器到底在解决什么问题如果你刚开始写 AndroidListView 大概率是第一个让你有点懵的控件。你明明往里面塞了数据界面却一片空白或者你照着教程写了setAdapter结果运行直接崩。问题往往不在 ListView 本身而在于你没选对 adapter。adapter 这个词翻译过来叫“适配器”你可以把它理解成一座桥左边是数据源数组、List、数据库查询结果右边是 ListView 里的一个个 item 视图。ListView 自己不知道怎么把一条数据变成一行界面adapter 就是干这个翻译工作的。Android 框架里常见的 adapter 有 BaseAdapter、ArrayAdapter、SimpleAdapter、SimpleCursorAdapter它们之间是继承关系能力从通用到专用逐层收窄。这篇文章面向初学者和中级开发者目标很明确把 BaseAdapter、ArrayAdapter、SimpleAdapter、SimpleCursorAdapter 的继承关系和适用边界讲清楚每个都给出最小可运行代码最后告诉你什么时候该用哪个。核心检索词就是 adapter 选型、ArrayAdapter 用法、SimpleAdapter 绑定、SimpleCursorAdapter 数据库、BaseAdapter 自定义。你跟着敲一遍基本就能判断自己项目里该用哪个了。先说结论方便你建立整体印象Adapter数据源是否需要自定义 item 布局典型场景ArrayAdapterString[] 或 List否用系统布局即可纯文字列表SimpleAdapterListMapString,Object是但不用写 ViewHolder图文混排、键值对数据SimpleCursorAdapterCursor是字段映射数据库查询结果展示BaseAdapter任意是完全自定义复杂 item、需要复用优化继承关系是这样的BaseAdapter 是根基ArrayAdapter、SimpleAdapter、CursorAdapter 都直接或间接继承自它CursorAdapter 是抽象类SimpleCursorAdapter 又继承自 CursorAdapter。所以你自定义 adapter 时继承 BaseAdapter本质上是在实现它那几个抽象方法。很多人一上来就继承 BaseAdapter觉得“万能”。但如果你只是显示一串城市名用 ArrayAdapter 三行代码就搞定没必要写几十行 getView。选型的核心判断标准是数据源长什么样、item 布局复不复杂、要不要跟数据库打交道。2. 用 TaoToken 快速生成并验证各 adapter 的最小代码写 adapter 代码时最烦的其实是样板代码实体类、布局文件、getView 里的 findViewById一遍遍重复。我现在的习惯是先用 TaoToken 把骨架生成出来再自己改细节效率高很多。TaoToken 是一个大模型 API 聚合平台你可以把它理解成一个统一的入口用一套 API Key 就能调用多种模型。对 Android 开发来说它的用处在于你描述需求它直接给你可运行的 Java/Kotlin 代码片段省去查文档的时间。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。前置准备很简单三步第一步注册并拿到 API Key。登录后在控制台创建密钥地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建完记得复制保存页面关了就看不到了。第二步如果你想在对话里直接让模型帮你写 adapter 代码用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。把“帮我写一个 SimpleAdapter 绑定图文列表的最小示例”丢进去它会给你完整代码。第三步如果你是在 IDE 里用插件比如 Cline、Continue写代码需要配置 Base URL 和 Key。Base URL 填 https://taotoken.net/api Key 填你刚创建的。模型 ID 根据你选的模型填比如 claude 系列或 gpt 系列具体以文档为准。这里要提醒一句TaoToken 是帮你生成和验证代码的工具不是替代 Android Studio 的编辑器。你最终还是要回到 IDE 里编译运行。它的价值在于把“查 API 怎么写”这件事压缩成一次对话。对于长期要写 Android 项目、经常需要生成样板代码的同学可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 适合高频编码场景。如果你只是偶尔查一下 adapter 写法用模型对话就够了。我试过用这种方式生成 BaseAdapter 的 ViewHolder 优化模板比手写快不少而且不容易漏掉 convertView 复用逻辑。下面几节我会把每个 adapter 的完整代码给出来你可以直接复制也可以用 TaoToken 生成后对照修改。3. 四种 adapter 的可复制配置与代码实现这一节是重点每个 adapter 我都给出布局文件、Activity 代码和关键参数说明。你按顺序敲或者直接复制到项目里改包名即可。3.1 ArrayAdapter纯文字列表最快方案先看布局一个 ListView 就够RelativeLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent ListView android:idid/listView android:layout_widthmatch_parent android:layout_heightwrap_content/ /RelativeLayoutActivity 代码package com.example.arrayadapterdemo; import android.app.Activity; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; public class MainActivity extends Activity { private ListView listView; private String[] city {广州, 深圳, 北京, 上海, 香港, 澳门, 天津}; private ArrayAdapterString arrayAdapter; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView findViewById(R.id.listView); arrayAdapter new ArrayAdapter( MainActivity.this, android.R.layout.simple_list_item_1, city); listView.setAdapter(arrayAdapter); } }关键参数就三个Context、item 布局资源、数据源。android.R.layout.simple_list_item_1是系统自带的单行文本布局里面只有一个 TextView。如果你用simple_list_item_2它会给你两个 TextView但 ArrayAdapter 默认只填第一个第二个需要你自己处理所以纯文字列表就用simple_list_item_1。ArrayAdapter 的边界很清楚数据源是 String[] 或 Listitem 只显示文字。一旦你要显示图片它就不够用了。3.2 SimpleAdapter图文混排不用写 getViewSimpleAdapter 的数据源是ListMapString, Object每个 Map 代表一行key 是字段名value 是具体数据。它通过from数组和to数组做映射from 里写 Map 的 keyto 里写 item 布局中控件的 id。item 布局list_items.xml?xml version1.0 encodingutf-8? RelativeLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationhorizontal ImageView android:idid/items_imageView1 android:layout_widthwrap_content android:layout_heightwrap_content / TextView android:idid/items_textView1 android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginLeft52dp / /RelativeLayoutActivity 代码package com.example.simpleadapterdemo; import android.app.Activity; import android.os.Bundle; import android.widget.ListView; import android.widget.SimpleAdapter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class MainActivity extends Activity { private ListView listView; private String[] name {小明, 小华, 小梁, 小王, 小林, 小赵}; private int[] images {R.drawable.one, R.drawable.two, R.drawable.three, R.drawable.four, R.drawable.five, R.drawable.six}; private ListMapString, Object listMap new ArrayList(); Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView findViewById(R.id.listView); for (int i 0; i 6; i) { MapString, Object items new HashMap(); items.put(pic, images[i]); items.put(name, name[i]); listMap.add(items); } SimpleAdapter simpleAdapter new SimpleAdapter( MainActivity.this, listMap, R.layout.list_items, new String[]{pic, name}, new int[]{R.id.items_imageView1, R.id.items_textView1}); listView.setAdapter(simpleAdapter); } }注意 from 和 to 的顺序必须一一对应pic对应R.id.items_imageView1name对应R.id.items_textView1。顺序错了图片可能显示到文字控件上直接崩或者显示异常。SimpleAdapter 的优点是省去了 getView 的编写适合数据已经是键值对形式、item 布局固定的场景。缺点是它内部没有做 ViewHolder 优化数据量大时性能一般。3.3 BaseAdapter完全自定义性能可控BaseAdapter 是抽象类你必须实现getCount、getItem、getItemId、getView四个方法。它的数据源可以是任意类型item 布局完全由你决定。先定义实体类 Userpackage com.example.baseadapterdemo; public class User { private String name; private int pic; public String getName() { return name; } public void setName(String name) { this.name name; } public int getPic() { return pic; } public void setPic(int pic) { this.pic pic; } }item 布局items_layout.xml?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal ImageView android:idid/items_pic android:layout_width60dp android:layout_height60dp / TextView android:idid/items_name android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_vertical android:layout_marginLeft16dp / /LinearLayoutActivity 与内部 Adapterpackage com.example.baseadapterdemo; import android.app.Activity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import java.util.ArrayList; import java.util.List; public class MainActivity extends Activity { private ListView listView; private ListUser list; private int[] images {R.drawable.one, R.drawable.two, R.drawable.three, R.drawable.four, R.drawable.five}; private String[] names {张三, 李四, 王五, 赵六, 天启}; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView findViewById(R.id.listView); list new ArrayList(); for (int i 0; i 5; i) { User u new User(); u.setName(names[i]); u.setPic(images[i]); list.add(u); } MyBaseAdapter adapter new MyBaseAdapter(); listView.setAdapter(adapter); } class MyBaseAdapter extends BaseAdapter { Override public int getCount() { return list.size(); } Override public Object getItem(int position) { return list.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 LayoutInflater.from(MainActivity.this) .inflate(R.layout.items_layout, parent, false); holder new ViewHolder(); holder.pic convertView.findViewById(R.id.items_pic); holder.name convertView.findViewById(R.id.items_name); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } User u list.get(position); holder.pic.setImageResource(u.getPic()); holder.name.setText(u.getName()); return convertView; } } static class ViewHolder { ImageView pic; TextView name; } }这里我加了 ViewHolder 模式这是 BaseAdapter 相比 SimpleAdapter 的核心优势convertView 复用 控件引用缓存滑动时不会反复 findViewById长列表性能明显更好。inflate的第二个参数传parent而不是 null能让 item 的 layout 参数正确生效这是个容易踩的坑。3.4 SimpleCursorAdapter数据库查询结果直接绑定SimpleCursorAdapter 的数据源是 Cursor通常来自 SQLite 查询。它继承自 CursorAdapter内部通过字段名到控件 id 的映射来绑定数据。假设你有一个数据库表person查询后得到 Cursor代码大致如下String[] from {name, age}; int[] to {R.id.items_name, R.id.items_age}; SimpleCursorAdapter cursorAdapter new SimpleCursorAdapter( MainActivity.this, R.layout.items_layout, cursor, from, to, 0); listView.setAdapter(cursorAdapter);最后一个参数是 flags传 0 表示默认行为。from 里写的是 Cursor 的列名to 里写的是 item 布局的控件 id。Cursor 必须包含一个名为_id的列否则会报错这是 ListView 和 CursorAdapter 配合的硬性要求。SimpleCursorAdapter 适合“查询结果直接展示”的场景比如通讯录、订单列表。但如果你需要对 Cursor 做复杂转换或者 item 逻辑很重建议继承 CursorAdapter 自己实现 newView 和 bindView控制力更强。4. 验证请求与成功结果怎么确认 adapter 真的生效了代码写完怎么验证最直接的方式是运行 App看列表是否正常显示。但如果你想更系统地验证可以按下面的步骤来。第一步检查数据源大小。在setAdapter之前打一行日志Log.d(AdapterDebug, data size list.size());如果 size 是 0说明数据没准备好界面当然是空的。这一步能排除大部分“列表不显示”的问题。第二步确认 item 布局的根节点没有设置layout_heightmatch_parent。如果 item 根布局高度是 match_parentListView 里每个 item 会占满整屏看起来就像只显示了一条。正确做法是wrap_content。第三步用 TaoToken 的模型对话验证代码逻辑。把你不确定的 getView 代码贴进去问“这段代码有没有 convertView 复用问题”它会帮你检查。模型对话入口 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。第四步观察滑动性能。如果滑动时明显卡顿多半是 getView 里做了耗时操作或者没复用 convertView。用 BaseAdapter 时务必加 ViewHolder。成功的结果应该是列表正常显示所有数据滑动流畅点击 item 能正确响应。如果你用的是 SimpleCursorAdapter还要确认数据库查询返回的 Cursor 不为 null 且 moveToFirst 返回 true。这里给一个验证用的完整检查清单数据源非空 → item 布局高度为 wrap_content → adapter 已 setAdapter → getView 返回的 view 不为 null → Cursor 含 _id 列。按这个顺序排查基本能定位所有常见问题。5. 本篇常见报错排查从 401 到 reading choices这一节我把实际开发中容易遇到的报错列出来对照着改。报错一java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification这个错误通常出现在你修改了数据源却没有调用notifyDataSetChanged()。比如你往 list 里 add 了新数据必须通知 adapterlist.add(newUser); adapter.notifyDataSetChanged();注意notifyDataSetChanged 要在主线程调用。报错二java.lang.NullPointerException在 getView 里多半是 findViewById 返回 null原因通常是 item 布局里没有对应的 id或者你 inflate 时传了错误的布局文件。检查inflate的第一个参数是不是 item 布局而不是 Activity 的主布局。报错三SimpleCursorAdapter 报java.lang.IllegalArgumentException: column _id does not existCursor 查询时必须包含_id列。如果你的表主键不叫_id用AS _id别名SELECT id AS _id, name, age FROM person报错四使用 TaoToken API 时返回 401401 表示鉴权失败通常是 API Key 没填对或者没带在请求头里。检查你的请求头Authorization: Bearer sk-你的key如果你在 Cline 或 Continue 里配置Base URL 填https://taotoken.net/apiKey 填控制台创建的密钥。模型 ID 要填对比如claude-3-5-sonnet之类的具体名称填错会报模型不存在。报错五local proxy failed或连接超时这类错误一般是网络配置问题。检查你的 Base URL 是否完整有没有多写或少写/v1。TaoToken 的 API 入口是https://taotoken.net/api具体路径以文档为准。如果你在公司网络下确认没有额外的网络策略拦截。报错六reading choices相关错误这通常出现在解析模型返回结果时返回体里没有 choices 字段说明请求可能失败了。先看 HTTP 状态码如果是 4xx检查参数如果是 5xx稍后重试。把完整返回体贴到模型对话里问能快速定位。报错七OAuth 相关错误如果你用的是 Claude Code 之类的工具配置时可能需要 OAuth 流程。确保回调地址和配置一致token 没过期。具体配置参考文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。排查的核心思路是先看日志里的异常类型和行号再对照上面的清单。adapter 的问题 90% 出在数据源、布局 id、convertView 复用这三块。6. 选型建议与接入入口回到最初的问题什么时候用哪个 adapter如果你只是显示一串字符串用 ArrayAdapter代码最少。如果你要显示图文混排数据是键值对形式用 SimpleAdapter不用写 getView。如果你的数据来自数据库查询用 SimpleCursorAdapter直接绑定 Cursor。如果你需要复杂 item、高性能复用、或者数据源是自定义对象列表用 BaseAdapter配合 ViewHolder。实际项目中BaseAdapter 的自定义子类是最常见的因为业务 item 往往很复杂。但不要为了“显得专业”而滥用它简单场景用简单方案代码更好维护。如果你在写 adapter 时想快速生成模板代码或者遇到报错想找人帮你分析可以用 TaoToken。API Key 在控制台创建 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。需要长期编码辅助的看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。最后留一个实用技巧不管你用哪个 adapteritem 布局的根节点高度尽量用 wrap_contentgetView 里避免做耗时操作数据变化后记得 notifyDataSetChanged。这三条能帮你避开大部分坑。