
1. 先搞清楚 ListView 三种适配器到底该选谁ListView 是 Android 里最经典的列表组件虽然现在 RecyclerView 更主流但大量老项目、教学示例、嵌入式设备界面里仍然是 ListView 的天下。一个 ListView 界面由三部分组成ListView 组件本身负责滚动和回收适配器负责把数据映射成一行行 View数据源则是你要展示的内容。适配器选错了代码会又长又难维护选对了几十行就能跑通。这篇面向 Android 初学者把 ArrayAdapter、SimpleAdapter、SimpleCursorAdapter 三种适配器的选型逻辑讲清楚并给出可以直接复制的布局 XML 和 Java/Kotlin 配置代码。你跟着做完能跑出一个可滚动的列表界面并且知道什么场景该换哪种适配器。选型其实一句话能概括只显示一行纯文字用 ArrayAdapter每行要图片加多段文字甚至按钮用 SimpleAdapter数据来自 SQLite 查询结果Cursor用 SimpleCursorAdapter。下面这张表先给你一个整体印象后面每一节都会展开。适配器数据源类型每行能显示什么典型场景ArrayAdapterList 或数组单个 TextView设置项、简单菜单SimpleAdapterListMapString,Object图片、多段文字、按钮等商品列表、联系人卡片SimpleCursorAdapterCursor由 Cursor 列映射到 View数据库查询结果、通讯录很多初学者会问既然 SimpleAdapter 这么灵活为什么还要用 ArrayAdapter因为 ArrayAdapter 代码量最少系统内置布局直接可用不需要自己写 item 布局。反过来SimpleAdapter 虽然灵活但数据要用 List组织写起来啰嗦而且它不支持复杂交互逻辑。SimpleCursorAdapter 则是为数据库场景量身定做的省去了手动把 Cursor 转成 List 的步骤。还有一个容易踩的坑三种适配器的构造函数参数顺序和含义不一样尤其是 SimpleAdapter 的五个参数初学者经常把 from 和 to 数组的顺序写反导致界面显示空白或者直接崩溃。后面我会逐个拆解。如果你在配置过程中需要查 Android 相关的模型能力或者让 AI 帮你解释报错可以顺手用一下模型对话入口把错误日志贴进去问比翻文档快。地址是 https://taotoken.net/api 配合 API Key 就能调用。这一节先把选型逻辑立住下一节开始动手写代码。记住一个原则能用简单适配器解决的就别上复杂的ListView 的性能瓶颈往往不在适配器类型而在 getView 里有没有做 View 复用。2. 环境准备与 TaoToken 前置配置在写代码之前先把开发环境和辅助工具准备好。Android 部分你需要 Android Studio建议 Hedgehog 或更新版本、一个能跑的模拟器或者真机、以及基本的 Gradle 同步能力。这些属于标准配置不展开。重点说一下怎么用 TaoToken 来辅助你排查适配器相关的报错因为初学者最容易卡在类型不匹配、资源找不到这类问题上。TaoToken 是一个模型调用平台你可以把它理解成一个统一的 API 入口通过它调用不同的大模型来完成代码解释、报错分析、文档生成等任务。对于 Android 学习场景最实用的用法是把编译报错或者运行时异常贴给模型让它告诉你哪一行参数写错了。比如 SimpleAdapter 的 from 数组和 to 数组长度不一致时运行会抛 IllegalStateException这种错误搜索引擎不一定能精准命中但模型能直接定位。前置配置分三步。第一步拿到 API Key。访问 https://taotoken.net/api-keys 创建密钥注意这个 Key 只在创建时完整显示一次复制后妥善保存。第二步确认你要调用的模型 ID比如常见的对话模型或者代码模型模型 ID 在文档里有列表。第三步把 Base URL 和 Key 配置到你的调用工具里。如果你用的是命令行工具或者 IDE 插件配置通常是一个 JSON 或 TOML 文件。下面给一个通用的 JSON 配置片段路径按你实际使用的工具放置{ base_url: https://taotoken.net/api, api_key: sk-你的密钥, model: 你的模型ID, timeout: 60 }注意 base_url 后面不要多加斜杠也不要带 /v1 之类的后缀具体以文档为准。Key 不要硬编码到提交到 Git 的代码里建议用环境变量或者本地配置文件并加入 .gitignore。配置完成后你可以先用一个最简单的请求验证连通性。比如用 curl 发一条消息curl https://taotoken.net/api/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的密钥 \ -d { model: 你的模型ID, messages: [{role: user, content: 用一句话解释 ArrayAdapter 和 SimpleAdapter 的区别}] }如果返回正常的 JSON 且 choices 里有内容说明配置成功。这一步很关键因为后面排查 Android 报错时你要反复用到这个通道。如果返回 401说明 Key 不对或者没带上 Authorization 头如果返回 model not found说明模型 ID 写错了。对于长期做 Android 开发、需要频繁让模型帮忙读代码的场景可以考虑 Coding Plan额度更划算适合持续调用。入口在 https://taotoken.net/api 的套餐页面里能找到。环境准备好之后下一节开始写真正的 ListView 代码。我会按 ArrayAdapter、SimpleAdapter、SimpleCursorAdapter 的顺序每个都给完整的布局和 Java 代码你可以直接复制到项目里跑。3. 三种适配器可复制配置与代码实战这一节是全文的核心每个适配器都给出布局 XML、Java 代码、以及关键参数说明。你新建一个 Android 项目把代码贴进去就能跑。为了减少干扰每个示例都用独立的 Activity。3.1 ArrayAdapter一行文字的极简列表ArrayAdapter 适合只显示一行文本的场景。它的构造函数最常用的是三个参数Context、item 布局资源、数据 List。系统内置了 android.R.layout.simple_list_item_1就是一个 TextView直接用就行不用自己写 item 布局。先看主布局 activity_array.xml只需要一个 ListView?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical ListView android:idid/list_array android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayout然后是 Java 代码 ArrayAdapterActivity.javapackage com.example.listviewdemo; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; import androidx.appcompat.app.AppCompatActivity; import java.util.ArrayList; import java.util.List; public class ArrayAdapterActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_array); ListView listView findViewById(R.id.list_array); ListString data new ArrayList(); data.add(数据项 1); data.add(数据项 2); data.add(数据项 3); data.add(数据项 4); data.add(数据项 5); ArrayAdapterString adapter new ArrayAdapter( this, android.R.layout.simple_list_item_1, data); listView.setAdapter(adapter); } }关键点泛型 String 要和数据 List 的类型一致。如果你写 ArrayAdapter编译能过但运行时可能出问题。另外 simple_list_item_1 里的 text1 是系统固定的 IDArrayAdapter 会自动把字符串塞进去你不用管。3.2 SimpleAdapter图片加多段文字的商品列表SimpleAdapter 的灵活性来自自定义 item 布局。你需要准备一个 listviewrow.xml里面放 ImageView 和多个 TextView。数据用 ListMapString,Object 组织每个 Map 对应一行key 是自定义的字段名。先写 item 布局 listviewrow.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 android:padding10dp ImageView android:idid/item_image android:layout_width68dp android:layout_height65dp android:scaleTypecenterCrop / LinearLayout android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginStart10dp android:orientationvertical TextView android:idid/item_name android:layout_widthwrap_content android:layout_heightwrap_content android:textSize16sp android:textColor#222222 / TextView android:idid/item_price android:layout_widthwrap_content android:layout_heightwrap_content android:textSize14sp android:textColor#666666 / TextView android:idid/item_discount android:layout_widthwrap_content android:layout_heightwrap_content android:textSize14sp android:textColor#FF0000 / /LinearLayout /LinearLayout主布局 activity_simple.xml 同样只放一个 ListView?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical ListView android:idid/list_simple android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayoutJava 代码 SimpleAdapterActivity.javapackage com.example.listviewdemo; import android.os.Bundle; import android.widget.ListView; import android.widget.SimpleAdapter; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class SimpleAdapterActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_simple); ListView listView findViewById(R.id.list_simple); ListMapString, Object data new ArrayList(); MapString, Object item1 new HashMap(); item1.put(img, R.drawable.ic_launcher_background); item1.put(name, 摩托罗拉 XT711); item1.put(price, 2669 元); item1.put(discount, 9 折); data.add(item1); MapString, Object item2 new HashMap(); item2.put(img, R.drawable.ic_launcher_background); item2.put(name, iPhone 4S); item2.put(price, 2669 元); item2.put(discount, 9 折); data.add(item2); MapString, Object item3 new HashMap(); item3.put(img, R.drawable.ic_launcher_background); item3.put(name, 三星 11 5G); item3.put(price, 2669 元); item3.put(discount, 9 折); data.add(item3); SimpleAdapter adapter new SimpleAdapter( this, data, R.layout.listviewrow, new String[]{img, name, price, discount}, new int[]{R.id.item_image, R.id.item_name, R.id.item_price, R.id.item_discount}); listView.setAdapter(adapter); listView.setOnItemClickListener((parent, view, position, id) - Toast.makeText(SimpleAdapterActivity.this, 你选择的是 data.get(position).get(name), Toast.LENGTH_SHORT).show()); } }这里最容易错的地方是 from 和 to 两个数组的对应关系。from 是 Map 里的 key 数组to 是 item 布局里的 View ID 数组两者长度必须一致顺序一一对应。上面代码里 from 第一个是 imgto 第一个是 R.id.item_image这样图片才会正确显示。如果你把顺序写反比如 from 是 {name,img} 而 to 是 {R.id.item_image, R.id.item_name}运行时会抛异常或者显示错乱。3.3 SimpleCursorAdapter从数据库 Cursor 直接映射SimpleCursorAdapter 的数据源是 Cursor通常来自 ContentResolver 查询或者 SQLite 查询。它的构造函数需要 Cursor、from 列名数组、to View ID 数组。注意 Cursor 必须包含一个名为 _id 的列否则会报错这是 Android 的硬性要求。主布局 activity_cursor.xml?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical ListView android:idid/list_cursor android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayoutJava 代码 SimpleCursorAdapterActivity.java这里用查询通讯录举例注意需要申请 READ_CONTACTS 权限package com.example.listviewdemo; import android.database.Cursor; import android.os.Bundle; import android.provider.ContactsContract; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import androidx.appcompat.app.AppCompatActivity; public class SimpleCursorAdapterActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cursor); ListView listView findViewById(R.id.list_cursor); Cursor cursor getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{ ContactsContract.CommonDataKinds.Phone._ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER }, null, null, null); if (cursor null) { return; } SimpleCursorAdapter adapter new SimpleCursorAdapter( this, android.R.layout.simple_list_item_2, cursor, new String[]{ ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER }, new int[]{android.R.id.text1, android.R.id.text2}, 0); listView.setAdapter(adapter); } }注意最后一个参数 0 是 flags旧版本常用 FLAG_AUTO_REQUERY但那个常量已经废弃直接传 0 即可。另外 Cursor 的生命周期要管理好Activity 销毁时应该关闭简单场景可以用 startManagingCursor但更推荐在 onDestroy 里手动 cursor.close()。如果你想让模型帮你检查这段 Cursor 查询的列名是否正确可以把代码贴到模型对话里问入口是 https://taotoken.net/api 配合 Key 使用。三个适配器的代码都跑通之后你会发现它们的主布局几乎一样区别全在适配器的构造和 item 布局。这就是选型的意义数据形态决定适配器类型。4. 逐项运行验证与成功结果确认代码写完不代表跑通这一节给你一套逐项验证的步骤确保每个适配器都真的工作。验证的核心思路是先确认编译通过再确认界面渲染最后确认交互响应。第一步编译验证。在 Android Studio 里点击 Build Make Project观察 Build 窗口。如果出现 cannot find symbol 或者 resource not found通常是布局文件名拼错、ID 引用错误、或者 drawable 资源不存在。比如 SimpleAdapter 示例里用了 R.drawable.ic_launcher_background如果你项目里没有这个资源就换成自己项目里已有的图片或者临时用 android.R.drawable.ic_menu_gallery 代替。第二步注册 Activity。三个 Activity 都要在 AndroidManifest.xml 里声明否则启动会闪退。示例activity android:name.ArrayAdapterActivity / activity android:name.SimpleAdapterActivity / activity android:name.SimpleCursorAdapterActivity /第三步逐个启动验证。先跑 ArrayAdapterActivity预期结果是屏幕上出现五行文字可以上下滚动。如果列表空白检查 setAdapter 是否调用、数据 List 是否为空、ListView 的宽高是否是 match_parent。再跑 SimpleAdapterActivity预期结果是每行左边一张图右边三行文字名称、价格、折扣点击某一行弹出 Toast 显示商品名。如果图片不显示检查 ImageView 的宽高是否给了具体值以及 Map 里的 key 和 from 数组是否一致。如果点击没反应检查 setOnItemClickListener 是否在 setAdapter 之后调用。最后跑 SimpleCursorAdapterActivity这个需要动态申请 READ_CONTACTS 权限。在 Android 6.0 以上你必须在代码里请求权限否则 Cursor 返回空。最简单的验证方式是在设置里手动给应用授权或者加一段运行时权限请求代码。预期结果是显示通讯录里的姓名和号码每行两段文字。第四步用日志确认数据量。在每个适配器 setAdapter 之后加一行 LogLog.d(ListViewDemo, adapter count adapter.getCount());如果 count 是 0说明数据源为空问题出在数据准备阶段而不是适配器。如果 count 大于 0 但界面空白问题出在布局或者 View 映射。第五步验证滚动性能。往 ArrayAdapter 的数据里塞 100 条快速滑动观察是否卡顿。ArrayAdapter 和 SimpleAdapter 内部都做了 View 复用正常情况不会卡。如果你自己写了自定义 Adapter 而没复用 convertView才会卡。这也是为什么初学阶段优先用系统适配器。跑完这五步三个适配器都正常显示说明你已经掌握了 ListView 的基本用法。接下来是排错环节把常见的报错和处理方式列出来。5. 本篇常见报错排查这一节按真实报错信息来组织你遇到哪个就查哪个。每个报错都给出原因和修复方式。第一个高频报错java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification。这个通常出现在你修改了数据 List 但没有调用 adapter.notifyDataSetChanged()。修复方式是在数据变更后调用 notifyDataSetChanged并且确保数据变更在主线程。如果你用的是 SimpleCursorAdapter数据变更后要重新查询 Cursor 或者调用 swapCursor。第二个java.lang.IllegalArgumentException: column _id does not exist。这是 SimpleCursorAdapter 特有的Cursor 查询结果里必须包含 _id 列。修复方式是在 query 的 columns 数组里显式加上 _id比如 ContactsContract.CommonDataKinds.Phone._ID。如果你查的是自己建的 SQLite 表建表时主键要命名为 _id。第三个java.lang.NullPointerException在 setAdapter 那一行。常见原因是 findViewById 返回 null也就是布局里没有对应的 ID或者 setContentView 传错了布局文件。检查 R.layout.xxx 和布局里的 android:id 是否匹配。第四个SimpleAdapter 显示空白但 count 正常。原因是 from 数组里的 key 和 Map 里的 key 不一致或者 to 数组里的 ID 在 item 布局里不存在。逐个核对建议把 from 和 to 打印出来对照。第五个android.content.res.Resources$NotFoundException: Resource ID #0x0。说明你引用的资源 ID 是 0通常是 R.drawable.xxx 写错了或者 Map 里 put 的图片资源不存在。检查 drawable 目录下是否有对应文件。第六个401 错误如果你在用 TaoToken 辅助排查时遇到401 Unauthorized说明 API Key 无效或者没带上。检查 Authorization 头格式是否为Bearer sk-xxx以及 Key 是否被删除或过期。重新在 https://taotoken.net/api-keys 生成一个即可。第七个local proxy failed或者连接超时。这类错误通常是网络配置问题检查你的 base_url 是否写成了https://taotoken.net/api不要多加路径。如果公司网络有出口限制换一个网络环境再试。第八个OAuth 相关报错比如OAuth token expired。如果你用的是需要 OAuth 的工具重新走一遍授权流程确保 token 刷新正常。对于 API Key 方式调用一般不会遇到这个问题。第九个Cursor 查询返回 null。除了权限问题还要检查 ContentResolver 的 URI 是否正确。通讯录用 ContactsContract.CommonDataKinds.Phone.CONTENT_URI不要用已经废弃的 Contacts.People.CONTENT_URI后者在新版本上会直接抛异常。第十个列表项点击无响应。检查 ListView 的 item 布局里是否有可点击的控件比如 Button它们会抢占点击事件。如果 item 里有 Button需要给 Button 设置 focusablefalse或者改用其他交互方式。排查的核心方法是先看 Logcat 的完整堆栈定位到具体行号再对照上面的清单。不要只看最后一行异常信息前面的 Caused by 才是根因。如果你把堆栈贴给模型分析记得把包名和业务代码脱敏。6. 继续深入与工具链建议三个适配器跑通之后你可能会想ListView 是不是过时了答案是新项目优先用 RecyclerView但 ListView 仍然值得学因为它的适配器思想是相通的而且大量存量代码需要维护。理解 ArrayAdapter 的数据绑定、SimpleAdapter 的 Map 映射、SimpleCursorAdapter 的 Cursor 映射对你后面理解 RecyclerView.Adapter 的三件套onCreateViewHolder、onBindViewHolder、getItemCount有直接帮助。如果你想继续深入建议按这个顺序推进先给 SimpleAdapter 加上 ViewHolder 模式优化性能再尝试自己写一个继承 BaseAdapter 的自定义适配器最后迁移到 RecyclerView。每一步都可以让模型帮你 review 代码把关键片段贴到模型对话里问入口是 https://taotoken.net/api 配合 Key 使用。对于需要长期做 Android 开发、频繁调用模型辅助编码的场景Coding Plan 的额度更适合持续使用具体在 https://taotoken.net/api 的套餐页可以查看。接入文档在 https://taotoken.net/api 的文档区里面有各语言的调用示例。最后给一个实用技巧把三个适配器的示例代码放在同一个项目里用不同的按钮跳转这样你可以随时对比它们的差异。项目结构建议按功能分包adapter 相关的 Activity 放一个包布局文件按 Activity 命名资源文件统一管理。这样等你以后回头看能快速回忆起每种适配器的适用边界。代码跑通只是开始真正掌握的标准是给你一个新的列表需求你能在三十秒内判断该用哪种适配器并且知道 item 布局该怎么写。这个判断力来自反复练习把上面的示例改一改换成你自己的数据跑起来看看效果。