
1. 从一次列表卡顿说起Android 四大适配器到底怎么选如果你刚开始学 AndroidListView 大概率是你接触的第一个“看起来简单、写起来全是坑”的控件。它本身只负责滚动和回收真正决定每一行长什么样的是适配器Adapter。很多初学者写完setAdapter之后发现列表空白、图片错位、按钮点不动问题几乎都出在适配器选型和getView的写法上。这篇内容围绕 Android 四大适配器在 ListView 中的显示实现展开ArrayAdapter、SimpleAdapter、BaseAdapter、CursorAdapter。它们分别对应四种典型场景——纯文本列表、图文混合列表、带交互控件的复杂列表、数据库游标驱动的列表。我会把每个适配器的布局 XML、Java 代码、运行验证步骤都写成可直接复制的形式同时说明如何通过 TaoToken 统一 Key 与 API 通道完成接口配置和连通性验证让列表数据加载链路从本地假数据平滑过渡到真实接口。适合谁看Android 初学者、需要快速搭列表界面的开发者、以及想把列表数据源从硬编码换成网络请求的人。你不需要很深的 Android 功底但至少要能跑起一个 Activity。下面所有代码基于传统 View 体系不涉及 Compose方便你在老项目或教学示例里直接落地。先说结论ArrayAdapter 最省事但只能显示一行文字SimpleAdapter 扩展性好适合图文混排BaseAdapter 最灵活能塞按钮、复选框并处理点击CursorAdapter 专为数据库查询结果设计。选错适配器不会报错但会让你在getView里越写越乱。2. TaoToken 前置准备统一 Key 与 API 通道配置在把列表数据换成网络来源之前先把请求通道配好。TaoToken 的作用是提供一个统一的 API 入口和 Key 管理方式你不需要在代码里散落多个平台的地址和密钥。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 这个不加 UTM 参数。配置分三步拿 Key、确认 Base URL、选定 Model ID。这三件套在任何适配器接入网络数据时都要用到。进入控制台创建 API Key路径是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建后在 API Keys 页面复制页面地址 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。Key 只显示一次复制后先存到本地配置文件不要硬编码进 Java 源码。Base URL 统一写https://taotoken.net/api。Model ID 根据你要调用的模型填写比如对话类模型或编码类模型具体以文档为准文档入口 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你后续要做长期编码或 Agent 类任务可以了解 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。在 Android 项目里我建议把这三件套写进local.properties或gradle.properties再通过 BuildConfig 注入避免提交到仓库。示例配置片段如下路径与字段名保持一致# gradle.properties TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_MODEL_ID你的模型ID然后在build.gradle里读取并生成 BuildConfig 字段android { defaultConfig { buildConfigField String, TAOTOKEN_BASE_URL, \${TAOTOKEN_BASE_URL}\ buildConfigField String, TAOTOKEN_API_KEY, \${TAOTOKEN_API_KEY}\ buildConfigField String, TAOTOKEN_MODEL_ID, \${TAOTOKEN_MODEL_ID}\ } }这样在 Java 代码里就能用BuildConfig.TAOTOKEN_BASE_URL引用。注意Key 属于敏感信息正式项目应放在服务端代理客户端只拿临时凭证。本文为了演示链路可复现采用本地注入方式你上线前务必替换。配置完成后先别急着写适配器用一次最小请求验证通道是否通。可以用 curl 在电脑上测也可以直接在 Android 里发请求。验证请求属于接入排障环节遇到问题优先查 API Keys 和接入文档两个入口分别是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。3. 可复制配置四大适配器布局与代码实现这一节是核心每个适配器我都给出布局 XML 和 Java 代码你可以直接复制到项目里跑。先建一个基础 Activity 布局activity_main.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_view android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayout3.1 ArrayAdapter纯文本列表最快落地ArrayAdapter 适合只显示一行文字的列表。系统自带布局android.R.layout.simple_list_item_1就是一个 TextView。代码public class ArrayAdapterActivity extends Activity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView findViewById(R.id.list_view); ListString data new ArrayList(); data.add(测试数据1); data.add(测试数据2); data.add(测试数据3); data.add(测试数据4); ArrayAdapterString adapter new ArrayAdapter( this, android.R.layout.simple_list_item_1, data); listView.setAdapter(adapter); } }构造参数依次是上下文、行布局、数据源。注意泛型要匹配ArrayAdapterString对应ListString写错类型会在运行时抛异常。3.2 SimpleAdapter图文混排自定义行SimpleAdapter 的数据源是ListMapString, Object每个 Map 对应一行。先写行布局item_simple.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:padding8dp ImageView android:idid/img android:layout_width48dp android:layout_height48dp android:layout_marginEnd8dp / LinearLayout android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:orientationvertical TextView android:idid/title android:layout_widthwrap_content android:layout_heightwrap_content android:textSize18sp / TextView android:idid/info android:layout_widthwrap_content android:layout_heightwrap_content android:textSize13sp / /LinearLayout /LinearLayoutJava 代码public class SimpleAdapterActivity extends Activity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView findViewById(R.id.list_view); SimpleAdapter adapter new SimpleAdapter( this, getData(), R.layout.item_simple, new String[]{title, info, img}, new int[]{R.id.title, R.id.info, R.id.img}); listView.setAdapter(adapter); } private ListMapString, Object getData() { ListMapString, Object list new ArrayList(); MapString, Object map new HashMap(); map.put(title, G1); map.put(info, google1); map.put(img, R.drawable.a1); list.add(map); map new HashMap(); map.put(title, G2); map.put(info, google2); map.put(img, R.drawable.a2); list.add(map); return list; } }from数组是 Map 的键to数组是布局里的控件 id顺序一一对应。图片这里放的是 drawable 资源 idSimpleAdapter 会自动处理。3.3 BaseAdapter带按钮的复杂行与 ViewHolderBaseAdapter 需要自己实现getCount、getItem、getItemId、getView。行布局item_base.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:padding8dp ImageView android:idid/img android:layout_width48dp android:layout_height48dp android:layout_marginEnd8dp / LinearLayout android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:orientationvertical TextView android:idid/title android:layout_widthwrap_content android:layout_heightwrap_content android:textSize18sp / TextView android:idid/info android:layout_widthwrap_content android:layout_heightwrap_content android:textSize13sp / /LinearLayout Button android:idid/view_btn android:layout_widthwrap_content android:layout_heightwrap_content android:focusablefalse android:text查看 / /LinearLayout注意按钮上加了android:focusablefalse否则按钮会抢走 ListView 行的焦点导致整行点击失效。Java 代码public class BaseAdapterActivity extends Activity { private ListMapString, Object mData; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mData getData(); ListView listView findViewById(R.id.list_view); listView.setAdapter(new MyAdapter(this)); } private ListMapString, Object getData() { ListMapString, Object list new ArrayList(); MapString, Object map new HashMap(); map.put(title, G1); map.put(info, google1); map.put(img, R.drawable.a1); list.add(map); map new HashMap(); map.put(title, G2); map.put(info, google2); map.put(img, R.drawable.a2); list.add(map); return list; } public final class ViewHolder { public ImageView img; public TextView title; public TextView info; public Button viewBtn; } public class MyAdapter extends BaseAdapter { private LayoutInflater mInflater; public MyAdapter(Context context) { this.mInflater LayoutInflater.from(context); } Override public int getCount() { return mData.size(); } Override public Object getItem(int position) { return mData.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) { holder new ViewHolder(); convertView mInflater.inflate(R.layout.item_base, null); holder.img convertView.findViewById(R.id.img); holder.title convertView.findViewById(R.id.title); holder.info convertView.findViewById(R.id.info); holder.viewBtn convertView.findViewById(R.id.view_btn); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } MapString, Object item mData.get(position); holder.img.setBackgroundResource((Integer) item.get(img)); holder.title.setText((String) item.get(title)); holder.info.setText((String) item.get(info)); holder.viewBtn.setOnClickListener(v - Toast.makeText(BaseAdapterActivity.this, 点击了第 position 行, Toast.LENGTH_SHORT).show()); return convertView; } } }ViewHolder 模式是必须的它把 findViewById 的结果缓存到 tag 里避免每次 getView 都重新查找控件。列表滑动时的卡顿八成是因为没写 ViewHolder。3.4 CursorAdapter数据库游标驱动列表CursorAdapter 直接吃 Cursor适合查询结果展示。这里用SimpleCursorAdapter演示查询系统联系人需要权限在AndroidManifest.xml里加uses-permission android:nameandroid.permission.READ_CONTACTS /代码public class CursorAdapterActivity extends Activity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView findViewById(R.id.list_view); Cursor cursor getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null); startManagingCursor(cursor); SimpleCursorAdapter adapter new SimpleCursorAdapter( this, android.R.layout.simple_list_item_1, cursor, new String[]{ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, new int[]{android.R.id.text1}, 0); listView.setAdapter(adapter); } }startManagingCursor让 Activity 托管 Cursor 生命周期省去手动 close。注意 Android 高版本对权限和 Cursor 管理更严格实际项目建议用 Loader 或 Room 配合。4. 验证请求与成功结果从假数据到真实接口四个适配器跑通后把数据源换成 TaoToken 接口返回的内容才算完成链路验证。这里以 ArrayAdapter 为例用 HttpURLConnection 发一个请求把返回的文本解析成列表项。先确认三件套已注入 BuildConfig。请求代码private void loadFromApi() { new Thread(() - { try { URL url new URL(BuildConfig.TAOTOKEN_BASE_URL /v1/chat/completions); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setRequestProperty(Content-Type, application/json); conn.setRequestProperty(Authorization, Bearer BuildConfig.TAOTOKEN_API_KEY); conn.setDoOutput(true); String body {\model\:\ BuildConfig.TAOTOKEN_MODEL_ID \,\messages\:[{\role\:\user\,\content\:\列出三条测试数据\}]}; try (OutputStream os conn.getOutputStream()) { os.write(body.getBytes(StandardCharsets.UTF_8)); } int code conn.getResponseCode(); InputStream is code 200 ? conn.getInputStream() : conn.getErrorStream(); BufferedReader reader new BufferedReader(new InputStreamReader(is)); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } reader.close(); runOnUiThread(() - { if (code 200) { ListString data new ArrayList(); data.add(sb.toString()); ArrayAdapterString adapter new ArrayAdapter( this, android.R.layout.simple_list_item_1, data); listView.setAdapter(adapter); } else { Toast.makeText(this, 请求失败 code code, Toast.LENGTH_LONG).show(); } }); } catch (Exception e) { runOnUiThread(() - Toast.makeText(this, 异常: e.getMessage(), Toast.LENGTH_LONG).show()); } }).start(); }成功时列表会显示接口返回的文本。如果只是想验证模型对话是否通可以用模型对话页面直接测地址 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。验证通过后把返回的 JSON 解析成ListMapString, Object就能喂给 SimpleAdapter 或 BaseAdapter实现真正的网络列表。实测下来最容易出问题的是线程和 UI 更新。网络请求必须在子线程setAdapter必须在主线程用runOnUiThread包一层。另外记得在AndroidManifest.xml加网络权限uses-permission android:nameandroid.permission.INTERNET /5. 本篇常见错排查401、local proxy failed 与 choices 解析接入过程中会遇到几类典型报错逐个对照排查。第一类401 Unauthorized。说明 Key 无效或没带上。检查Authorization头是不是Bearer加 Key中间有空格。Key 是否复制完整有没有多余换行。如果 Key 刚创建确认没有在控制台被删除。排查入口在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。第二类local proxy failed。这类报错通常出现在本地网络环境或代理配置异常时。先确认 Base URL 写的是https://taotoken.net/api没有多余路径。再检查设备网络是否正常模拟器可以换真机试。如果项目里用了 OkHttp 并配置了代理把代理配置去掉再试。这个错误和适配器无关是请求通道问题。第三类reading choices 相关解析错误。接口返回的 JSON 结构和你的解析代码不匹配时会报这个。先打印原始响应体确认字段名。常见的是把choices当成数组却按对象解析或者message.content层级写错。建议先用模型对话页面看一次标准返回结构再对照写解析。第四类OAuth 或鉴权相关报错。如果你用的是需要 OAuth 的调用方式确认 token 是否过期。TaoToken 的 Key 方式不需要 OAuth 流程直接用 Bearer 即可。若你混用了其他鉴权方式统一改回 Key。第五类ListView 显示空白。先看getCount返回值是不是 0再看数据源是否为空。BaseAdapter 里如果getItem返回 null 不影响显示但getCount必须返回真实数量。还有一种是布局 id 写错findViewById返回 null运行时崩溃或空白。第六类按钮点击无效。回到 3.3 节确认按钮加了android:focusablefalse并且点击事件是在getView里设置的。如果按钮抢焦点整行点击也会失效。排查顺序建议先确认 Key 和 Base URL再确认网络权限最后看解析逻辑。每一步都用最小请求验证不要一次改太多。6. 继续往下走把列表接入真实业务四个适配器跑通、接口验证通过之后你可以按场景选型纯文本用 ArrayAdapter图文用 SimpleAdapter带交互用 BaseAdapter数据库用 CursorAdapter。真实项目里列表数据往往来自网络建议把请求封装成 Repository适配器只负责展示职责分离后getView会清爽很多。如果你后续要做长期编码或 Agent 类任务可以了解 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。需要直接和模型对话验证返回结构用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。接入文档和 Key 管理分别在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 和 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。最后留一个我踩过的坑BaseAdapter 的getView里不要做耗时操作图片加载用 Glide 或 Coil否则滑动会明显掉帧。ViewHolder 缓存的是控件引用不是数据数据每次都要重新 set。把这两点记住列表性能基本不会出大问题。