1. 从一次列表卡顿说起:Android 四大适配器到底怎么选
如果你刚开始学 Android,ListView 大概率是你接触的第一个“看起来简单、写起来全是坑”的控件。它本身只负责滚动和回收,真正决定每一行长什么样的,是适配器(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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 根地址是 https://taotoken.net/api (这个不加 UTM 参数)。
配置分三步:拿 Key、确认 Base URL、选定 Model ID。这三件套在任何适配器接入网络数据时都要用到。进入控制台创建 API Key,路径是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建后在 API Keys 页面复制,页面地址 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。Key 只显示一次,复制后先存到本地配置文件,不要硬编码进 Java 源码。
Base URL 统一写https://taotoken.net/api。Model ID 根据你要调用的模型填写,比如对话类模型或编码类模型,具体以文档为准,文档入口 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你后续要做长期编码或 Agent 类任务,可以了解 Coding Plan,地址 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
在 Android 项目里,我建议把这三件套写进local.properties或gradle.properties,再通过 BuildConfig 注入,避免提交到仓库。示例配置片段如下,路径与字段名保持一致:
# gradle.properties TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的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_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 和 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
3. 可复制配置:四大适配器布局与代码实现
这一节是核心,每个适配器我都给出布局 XML 和 Java 代码,你可以直接复制到项目里跑。先建一个基础 Activity 布局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/list_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>3.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); List<String> data = new ArrayList<>(); data.add("测试数据1"); data.add("测试数据2"); data.add("测试数据3"); data.add("测试数据4"); ArrayAdapter<String> adapter = new ArrayAdapter<>( this, android.R.layout.simple_list_item_1, data); listView.setAdapter(adapter); } }构造参数依次是上下文、行布局、数据源。注意泛型要匹配,ArrayAdapter<String>对应List<String>,写错类型会在运行时抛异常。
3.2 SimpleAdapter:图文混排自定义行
SimpleAdapter 的数据源是List<Map<String, Object>>,每个 Map 对应一行。先写行布局item_simple.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="horizontal" android:padding="8dp"> <ImageView android:id="@+id/img" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginEnd="8dp" /> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" /> <TextView android:id="@+id/info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" /> </LinearLayout> </LinearLayout>Java 代码:
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 List<Map<String, Object>> getData() { List<Map<String, Object>> list = new ArrayList<>(); Map<String, 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 资源 id,SimpleAdapter 会自动处理。
3.3 BaseAdapter:带按钮的复杂行与 ViewHolder
BaseAdapter 需要自己实现getCount、getItem、getItemId、getView。行布局item_base.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="horizontal" android:padding="8dp"> <ImageView android:id="@+id/img" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginEnd="8dp" /> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" /> <TextView android:id="@+id/info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="13sp" /> </LinearLayout> <Button android:id="@+id/view_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:focusable="false" android:text="查看" /> </LinearLayout>注意按钮上加了android:focusable="false",否则按钮会抢走 ListView 行的焦点,导致整行点击失效。Java 代码:
public class BaseAdapterActivity extends Activity { private List<Map<String, 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 List<Map<String, Object>> getData() { List<Map<String, Object>> list = new ArrayList<>(); Map<String, 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(); } Map<String, 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:name="android.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) { List<String> data = new ArrayList<>(); data.add(sb.toString()); ArrayAdapter<String> 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_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。验证通过后,把返回的 JSON 解析成List<Map<String, Object>>,就能喂给 SimpleAdapter 或 BaseAdapter,实现真正的网络列表。
实测下来,最容易出问题的是线程和 UI 更新。网络请求必须在子线程,setAdapter必须在主线程,用runOnUiThread包一层。另外记得在AndroidManifest.xml加网络权限:
<uses-permission android:name="android.permission.INTERNET" />5. 本篇常见错排查:401、local proxy failed 与 choices 解析
接入过程中会遇到几类典型报错,逐个对照排查。
第一类:401 Unauthorized。说明 Key 无效或没带上。检查Authorization头是不是Bearer加 Key,中间有空格。Key 是否复制完整,有没有多余换行。如果 Key 刚创建,确认没有在控制台被删除。排查入口在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
第二类: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:focusable="false",并且点击事件是在getView里设置的。如果按钮抢焦点,整行点击也会失效。
排查顺序建议:先确认 Key 和 Base URL,再确认网络权限,最后看解析逻辑。每一步都用最小请求验证,不要一次改太多。
6. 继续往下走:把列表接入真实业务
四个适配器跑通、接口验证通过之后,你可以按场景选型:纯文本用 ArrayAdapter,图文用 SimpleAdapter,带交互用 BaseAdapter,数据库用 CursorAdapter。真实项目里,列表数据往往来自网络,建议把请求封装成 Repository,适配器只负责展示,职责分离后getView会清爽很多。
如果你后续要做长期编码或 Agent 类任务,可以了解 Coding Plan,地址 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。需要直接和模型对话验证返回结构,用模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。接入文档和 Key 管理分别在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 和 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
最后留一个我踩过的坑:BaseAdapter 的getView里不要做耗时操作,图片加载用 Glide 或 Coil,否则滑动会明显掉帧。ViewHolder 缓存的是控件引用,不是数据,数据每次都要重新 set。把这两点记住,列表性能基本不会出大问题。