ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android学习笔记---19_采用ListView实现数据列表显示,以及各种适配器使用,和如何写自己的适配器(TaoToken 统一 Key 配置版)

Android学习笔记---19_采用ListView实现数据列表显示,以及各种适配器使用,和如何写自己的适配器(TaoToken 统一 Key 配置版) 1. 从一次column _id does not exist崩溃说起如果你正在学 Android 的 ListView大概率会经历这样一个瞬间代码照着教程敲完了SimpleCursorAdapter一挂上去应用直接闪退Logcat 里躺着一条java.lang.IllegalArgumentException: column _id does not exist。这个报错几乎是每个 Android 初学者都会踩的坑它本身不复杂但背后牵扯出的是整个 ListView 适配器体系的设计逻辑。这篇笔记就围绕「Android ListView 数据列表显示 适配器选型 自定义适配器」展开。我会把ArrayAdapter、SimpleAdapter、SimpleCursorAdapter、BaseAdapter这几种适配器放在同一个通讯录场景里对比讲清楚什么时候该用哪个、为什么SimpleCursorAdapter对列名这么挑剔、自定义适配器里convertView和ViewHolder到底在优化什么。同时因为现在很多 Android 项目会接入大模型能力做智能助手、代码补全或者数据解析我也会给出一套 TaoToken 统一 Key 的配置骨架让你在跑通列表绑定的同时顺手把 API 通道也验证掉。适合谁看刚学完 Activity 和 SQLite、准备啃 ListView 的 Android 新手被适配器绕晕、想一次性理清选型思路的同学以及想在 Android 工程里接入统一模型 API、但不想每个项目重复配 Key 的开发者。先说结论ListView 的显示本质是「数据源 → 适配器 → 条目布局」三段式适配器就是中间的翻译官。你选哪个翻译官取决于你的数据长什么样。2. TaoToken 前置统一 Key 与 API 通道在 Android 项目里的位置在正式写适配器之前先把 API 通道这块理清楚。很多同学做 Android 练习项目时模型调用是散落在各个 Activity 里的Key 硬编码、BaseUrl 到处复制换个项目就得重新找一遍。TaoToken 的思路是提供一个统一的 API 入口你只需要维护一份 Key 和一份 BaseUrl所有项目共用。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注意 API 地址后面不加任何 UTM 参数保持干净。在 Android 项目里我建议把配置放在local.properties或者一个独立的config文件里不要写死在 Java 代码中。原因很简单练习项目经常要复制粘贴配置和业务代码分离之后复制过去改一个 Key 就能跑。你需要先拿到 Key入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。拿到之后Android 侧的配置骨架大概长这样我放在app/src/main/assets/config.toml里# app/src/main/assets/config.toml [taotoken] base_url https://taotoken.net/api api_key sk-你的Key model claude-sonnet-4-5 timeout_seconds 30 [listview] page_size 20如果你更习惯 JSON也可以放settings.json{ taotoken: { base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-5, timeout_seconds: 30 }, listview: { page_size: 20 } }这里要强调一点Key 不要提交到 Git。练习项目也养成习惯把config.toml加进.gitignore仓库里只留一个config.example.toml。我见过太多人练习项目传上 GitHubKey 泄露了还不知道。配置好之后Android 侧读取配置的代码可以写成一个单例public class ApiConfig { private static ApiConfig instance; public final String baseUrl; public final String apiKey; public final String model; private ApiConfig(Context context) { // 实际项目建议用 Properties 或 JSON 解析 assets 里的文件 this.baseUrl https://taotoken.net/api; this.apiKey BuildConfig.TAOTOKEN_KEY; this.model claude-sonnet-4-5; } public static synchronized ApiConfig get(Context context) { if (instance null) { instance new ApiConfig(context.getApplicationContext()); } return instance; } }把 Key 放进BuildConfig是通过build.gradle的buildConfigField注入的这样代码里不出现明文也方便不同构建变体用不同的 Key。这一步做完你的 Android 项目就有了统一的模型调用通道后面不管做列表智能排序还是数据解析都走这一份配置。3. 可复制配置四种适配器的选型与代码骨架现在进入正题。ListView 的适配器体系我按「数据源类型」来分这样最好记。3.1 ArrayAdapter最简单适合纯字符串列表ArrayAdapter是最基础的适配器数据源就是一个ListString或者字符串数组。它默认给你一个android.R.layout.simple_list_item_1的条目布局一行文字。适合做设置项、简单菜单这种场景。ListString names Arrays.asList(老周, 老方, 老李); ArrayAdapterString adapter new ArrayAdapter( this, android.R.layout.simple_list_item_1, names); listView.setAdapter(adapter);它的局限也很明显只能显示一个字符串没法做「姓名 电话 存款」这种多字段布局。所以通讯录场景它不够用。3.2 SimpleAdapter多字段但数据源是 MapSimpleAdapter解决的是「一个条目有多个字段」的问题。它的数据源是ListHashMapString, Object每个 Map 代表一行key 对应字段名value 是字段值。然后你告诉它哪个 key 映射到哪个控件 id。ListHashMapString, Object data new ArrayList(); for (Person p : persons) { HashMapString, Object item new HashMap(); item.put(name, p.getName()); item.put(phone, p.getPhone()); item.put(amount, p.getAmount()); data.add(item); } SimpleAdapter adapter new SimpleAdapter( this, data, R.layout.item, new String[]{name, phone, amount}, new int[]{R.id.name, R.id.phone, R.id.amount}); listView.setAdapter(adapter);注意from数组和to数组必须一一对应顺序错了字段就串位。SimpleAdapter的优点是快不用自己写适配器缺点是数据得先转成 Map多了一层转换而且字段类型是Object取值时容易出类型转换问题。3.3 SimpleCursorAdapter直接绑 Cursor但列名有硬性要求SimpleCursorAdapter是给数据库查询结果用的数据源是Cursor。它省去了「Cursor 转 List」这一步直接绑定。但它的坑就是开头那个报错Cursor 里必须有一个叫_id的列。为什么因为SimpleCursorAdapter内部依赖_id来做条目标识ListView的OnItemClickListener回调里的long id参数就是从这来的。如果你的表主键叫personidCursor 里就没有_id直接抛IllegalArgumentException。解决办法有两个。第一个是把表主键直接命名成_idCREATE TABLE person ( _id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, phone TEXT, amount INTEGER );第二个是不改表结构在查询时用别名SELECT personid AS _id, name, phone, amount FROM person ORDER BY personid ASC LIMIT ?, ?;我个人推荐第二种因为已有项目改表结构成本高别名更灵活。查询代码public Cursor getCursorScrollData(int offset, int maxResult) { SQLiteDatabase db helper.getReadableDatabase(); return db.rawQuery( SELECT personid AS _id, name, phone, amount FROM person ORDER BY personid ASC LIMIT ?, ?, new String[]{String.valueOf(offset), String.valueOf(maxResult)}); }然后绑定Cursor cursor personService.getCursorScrollData(0, 20); SimpleCursorAdapter adapter new SimpleCursorAdapter( this, R.layout.item, cursor, new String[]{name, phone, amount}, new int[]{R.id.name, R.id.phone, R.id.amount}, 0); listView.setAdapter(adapter);注意最后一个参数flags传0表示默认行为。SimpleCursorAdapter还有个隐藏要求Cursor 必须是可滚动的查询时别用moveToFirst之后又关闭。3.4 BaseAdapter自定义适配器性能优化的主战场当你需要完全控制条目渲染逻辑或者数据源是自定义实体类ListPerson就该自己写BaseAdapter了。这也是面试和实战里最常考的部分。自定义适配器的核心是四个方法getCount、getItem、getItemId、getView。前三个简单重点是getView。public class PersonAdapter extends BaseAdapter { private final ListPerson persons; private final int resource; private final LayoutInflater inflater; public PersonAdapter(Context context, ListPerson persons, int resource) { this.persons persons; this.resource resource; this.inflater LayoutInflater.from(context); } Override public int getCount() { return persons.size(); } Override public Object getItem(int position) { return persons.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(resource, parent, false); holder new ViewHolder(); holder.nameView convertView.findViewById(R.id.name); holder.phoneView convertView.findViewById(R.id.phone); holder.amountView convertView.findViewById(R.id.amount); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } Person person persons.get(position); holder.nameView.setText(person.getName()); holder.phoneView.setText(person.getPhone()); holder.amountView.setText(String.valueOf(person.getAmount())); return convertView; } private static class ViewHolder { TextView nameView; TextView phoneView; TextView amountView; } }这里有两个关键点。第一convertView是 ListView 的回收机制屏幕外的条目会被回收滑回来时复用所以convertView不为 null 时不要重新 inflate。第二ViewHolder把findViewById的结果缓存到convertView的 tag 里避免每次getView都做一遍 findViewById。这两步做完列表滑动才会流畅。对比一下不优化的写法每次getView都inflate一遍、都findViewById一遍列表一长就卡。我实测过100 条数据不优化能明显感觉到掉帧优化后基本无感。3.5 四种适配器选型对照适配器数据源适用场景主要限制ArrayAdapterListString单行文本列表只能显示一个字段SimpleAdapterListMap多字段、数据已在内存需转 Map类型弱SimpleCursorAdapterCursor数据库查询直出必须有_id列BaseAdapter任意 ListT复杂条目、需优化需自己实现全部方法选型口诀纯文本用 Array内存多字段用 Simple数据库直出用 Cursor要性能要控制用 Base。4. 验证请求跑通列表绑定与 API 通道配置和代码都写完了接下来验证两件事列表能不能正常显示API 通道能不能通。4.1 列表绑定验证先在 Activity 里准备好数据用自定义适配器挂上去private void showCustomList() { ListPerson persons personService.getScrollData(0, 20); PersonAdapter adapter new PersonAdapter(this, persons, R.layout.item); listView.setAdapter(adapter); listView.setOnItemClickListener((parent, view, position, id) - { Person person (Person) parent.getItemAtPosition(position); Toast.makeText(getApplicationContext(), 点击了 person.getName(), Toast.LENGTH_SHORT).show(); }); }运行后你应该看到三列数据姓名、电话、存款点击某一行弹出对应姓名。如果列表空白先检查getCount返回的是不是 0再检查item.xml里的控件 id 和ViewHolder里findViewById的是不是一致。4.2 API 通道验证列表跑通后验证 TaoToken 通道。最简单的办法是在一个按钮里发一次请求把返回结果打到 Logcat。用 OkHttp 举例private void verifyApiChannel() { OkHttpClient client new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .build(); String json {\model\:\ ApiConfig.get(this).model \, \messages\:[{\role\:\user\,\content\:\回复 OK\}]}; Request request new Request.Builder() .url(ApiConfig.get(this).baseUrl /v1/messages) .addHeader(x-api-key, ApiConfig.get(this).apiKey) .addHeader(anthropic-version, 2023-06-01) .addHeader(content-type, application/json) .post(RequestBody.create(json, MediaType.parse(application/json))) .build(); client.newCall(request).enqueue(new Callback() { Override public void onFailure(Call call, IOException e) { Log.e(TaoToken, 请求失败, e); } Override public void onResponse(Call call, Response response) throws IOException { Log.d(TaoToken, 状态码 response.code()); Log.d(TaoToken, 响应 response.body().string()); } }); }状态码 200 且响应里有内容说明通道通了。如果返回 401检查 Key返回 404检查 BaseUrl 是不是https://taotoken.net/api别多加斜杠或者路径。想先在网页上确认模型可用可以直接用模型对话入口 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 发一条消息试试确认 Key 和模型名没问题再回到 Android 里调。如果你打算长期在 Android 项目里做编码辅助或者 Agent 类功能可以了解 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它更适合持续性的开发场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 参数细节以文档为准。5. 本篇常见错排查这一节把我在 ListView 和适配器上踩过的坑集中列一下你遇到问题可以先对照。报错column _id does not existSimpleCursorAdapter的 Cursor 缺少_id列。解决方式是查询时用SELECT personid AS _id, ...或者把表主键命名为_id。这是最高频的报错没有之一。列表显示空白先看getCount返回值。如果数据源是 Cursor检查cursor.getCount()如果是 List检查是不是空集合。另外确认setAdapter之后有没有再改数据源却没调notifyDataSetChanged。滑动卡顿getView里没有复用convertView或者每次都在findViewById。按第 3.4 节的 ViewHolder 写法改。点击条目拿到的数据不对getItemAtPosition(position)返回的是适配器里的对象。用SimpleCursorAdapter时返回的是 Cursor需要cursor.moveToPosition(position)再取值用自定义适配器时返回的是你的实体类直接强转。字段串位SimpleAdapter的from和to数组顺序不一致。两个数组必须严格一一对应。API 返回 401Key 错了或者没带上。检查请求头里x-api-key的值注意别把Bearer和x-api-key混用具体用哪个以接入文档为准。API 返回 404BaseUrl 写错了。正确是https://taotoken.net/api不要写成https://taotoken.net/api/v1再加/v1/messages会变成双 v1。网络请求在主线程Android 不允许主线程做网络请求会抛NetworkOnMainThreadException。用 OkHttp 的enqueue异步或者放到子线程。Key 泄露config.toml提交到了 Git。立刻去控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 吊销旧 Key重新生成并把配置文件加进.gitignore。6. 继续往下走把适配器思路用到更多控件ListView 的适配器体系搞明白之后GridView、RecyclerView 都是同一套思路的延伸。GridView 就是网格版的 ListView适配器用法几乎一样只是条目布局换成网格。RecyclerView 是更现代的替代品把getView拆成了onCreateViewHolder和onBindViewHolder但 ViewHolder 复用的思想是一脉相承的。如果你在 Android 项目里要接入模型能力做智能列表比如自动分类、摘要生成建议把 Key 管理统一到 TaoToken 这一层入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。Claude Code 相关的接入可以参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 。最后留一个练习把第 3.4 节的自定义适配器改成支持两种条目类型比如普通联系人和置顶联系人用不同布局在getViewTypeCount和getItemViewType里做区分。这个练完你对适配器的理解就到位了。
返回列表