Adapter 的 getCount/getItemId 分不清?TaoToken 给 Codex 配 Key 后逐条对照

发布时间:2026/9/18 12:14:41
Adapter 的 getCount/getItemId 分不清?TaoToken 给 Codex 配 Key 后逐条对照
Adapter 的getCount()、getItem()、getItemId()分不清是自定义 ListAdapter 时最常见的一类坑。先去 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建一把 Key把 Codex 接到 https://taotoken.net/api然后让 Codex 对着android.widget.Adapter的方法签名把你写的子类逐条核对。整条路径不需要你背文档但需要你把真实代码贴出来——Codex 只负责比对和解释Adapter 里那三个返回值到底该写什么最终还是你改。为什么这件事值得单独做一遍因为这三个方法的错误不会在编译期报出来。getCount()写成return 1;代码照样编过只是列表永远只显示一行getItem()写成return position;只有在你getView里把它强转成实体类的那一刻才炸ClassCastExceptiongetItemId()写成return 0;onItemClick的第四个参数id就永远是 0你还以为是点击事件没传参。三个方法各自独立错误却会在同一个界面上叠加出现这就是难排查的原因。这篇的顺序是先把 Codex 这条执行通道配通再用它去对照原文的 Adapter 方法签名。TaoToken 在这里只提供 Key 和 Base URL 两样东西它不替代android.widget.Adapter也不替你看 Android 源码。1. 从「列表只显示一行」说起三个返回值谁错了1.1 三个一眼看不出原因的现象第一个现象ListView明明有 20 条数据界面上只有一条而且不是第一条是某种随机位置的一条。这种情况十有八九是getCount()的返回值跟数据源脱钩了比如它在某个分支里返回了常量或者返回的是parent.getChildCount()这类当前屏幕上已经渲染出来的 View 数量——后者还会随着滚动不断变化。第二个现象点击第三行弹出来的是第一条的内容。这通常是getItem()返回了下标本身而getView()里又用getItem(position)去取数据取到的其实是Integer再强转就崩了如果碰巧没崩也会因为位运算或默认值的问题显示出错误内容。第三个现象onItemClick(AdapterView? parent, View view, int position, long id)里的id一直是 0。position是对的id是错的。看文档就知道这个id来自Adapter.getItemId(position)而BaseAdapter对它的默认实现就是return 0;。1.2 自定义 ListAdapter 为什么特别容易踩直接继承ArrayAdapter的时候你通常只需要覆写getView()因为getCount()、getItem()、getItemId()父类都实现好了。一旦改成继承BaseAdapter四个抽象方法全都要自己写少写一个不报编译错误因为getItemId有默认实现但行为就悄悄变了。更麻烦的是ArrayAdapter的默认getItemId()返回的是positionBaseAdapter的默认getItemId()返回的是0。两个父类默认值不同你在两个项目之间来回切换很容易把上一份代码里的直觉带过来。再叠加CursorAdapter它的getItemId()返回的是游标里的_id列三套语义混在一起分不清是正常的。2. 把 Codex 接到 TaoTokenconfig.toml 里改三个地方2.1 先去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建 Key准备工作只有一件打开 TaoToken 注册账号在控制台的 API Keys 页面创建一把新 Key复制出来先存到安全的地方。同一次访问里顺手看一眼模型广场当时的可用列表把你要用的模型 ID 记下来——后面config.toml里的model就填它不要凭印象写。Key 拿到之后注意区分两个地址注册、创建 Key、看用量走的是官网落地页真正填进 Codex 配置文件的 Base URL 是https://taotoken.net/api末尾不带/v1也不要带?utm_source那一串。2.2 ~/.codex/config.toml 的 model_provider 与 base_urlCodex 的自定义供应商写在~/.codex/config.toml里。文件不存在就新建已有内容就追加注意 TOML 的表头不能被拆散# ~/.codex/config.toml model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chatmodel_provider的值必须和下面表头[model_providers.taotoken]的名字一致这是最常见的低级错误——名字对不上Codex 会退回默认供应商然后你就开始怀疑 Key 有问题。env_key写的是环境变量名不是 Key 本身Key 不要硬编码进配置文件。然后把这个环境变量导出再启动 Codexexport TAOTOKEN_API_KEYYOUR_API_KEY codexPowerShell 环境里换成$env:TAOTOKEN_API_KEYYOUR_API_KEY codex2.3 模型 ID 以模型广场为准model字段只填你在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场里看到的 ID。这里不写具体型号是因为可用列表本身会调整照抄别人文章里的 ID 很容易踩空。填错模型 ID 的表现通常是 404 或者模型不存在的提示而不是 Key 错误。3. 逐条对照 android.widget.Adapter三个方法各返回什么3.1 getCount()返回数据集当前可展示的条数android.widget.Adapter对getCount()的定义是返回列表里有多少个 item 可以被展示。注意数据集里有多少条和屏幕上现在能看到几个 View是两件事前者是data.size()后者是ListView.getChildCount()。getCount()要返回的是前者。还有一个容易被忽略的点返回 0 是合法值。getCount()返回 0 的时候ListView会去显示它关联的 empty view。所以如果你同时设了setEmptyView()列表为空时界面是正常的这会掩盖getCount()写错的事实。3.2 getItem(int position)返回数据对象不是下标getItem(int position)的参数是行下标取值范围是0到getCount() - 1。返回值是这个位置上的数据对象类型由你的适配器自己决定。BaseAdapter里这个方法签名声明的是Object子类可以协变成具体类型比如public Note getItem(int position)。很多人在这里return position;因为参数和返回类型看起来都是整数。但在BaseAdapter里返回Integer之后getView()里的Note item (Note) getItem(position);就会抛ClassCastException: java.lang.Integer cannot be cast to Note。如果你在getView()里用getItem(position).toString()打印过东西又恰好看到打印出来的是0、1、2那就基本可以确定是这个错。3.3 getItemId(int position)稳定行 ID 与 hasStableIdsgetItemId(int position)返回的是这一行的 ID类型是long。它的语义只有和hasStableIds()一起看才完整hasStableIds()返回falseAdapter接口的默认值时框架不会依赖这个 ID 做状态保存返回true时框架会认为同一个 ID 一直对应同一条数据并用它来维护选中态。这就是返回 position 行不行的答案如果数据只追加不删除不乱序返回position勉强能用但hasStableIds()必须保持false。一旦数据会被插入、删除、排序position就不再稳定此时要么改成返回实体的真实 ID要么老老实实让hasStableIds()返回false。想让onItemClick的id有意义就得重写getItemId()并让hasStableIds()返回true。3.4 ArrayAdapter 与 BaseAdapter 的默认实现差在哪ArrayAdapter.getItemId(int position)的默认实现返回positionBaseAdapter.getItemId(int position)的默认实现返回0CursorAdapter则返回游标里_id列的值。三者的差异不是设计缺陷而是它们假设的数据源性质不同——数组天然有序游标天然有主键而BaseAdapter不知道你的数据长什么样只能返回一个安全的常量。所以自定义ListAdapter时第一件事是明确我的数据源有没有稳定主键。有就重写getItemId()返回它没有就别开hasStableIds()也别指望onItemClick的id参数有内容。4. 让 Codex 拿着这张表检查你的 ListAdapter4.1 可直接复制的对照提示词配通 Codex 之后把这段话和你的适配器代码一起贴进对话。注意 Codex 只做静态比对编译和运行都在你本地下面是我的 ListAdapter 完整代码。请对照 android.widget.Adapter 接口文档逐条检查 1. getCount() 返回值是否会随数据源变化是否存在写死常量或返回可见 View 数量的情况 2. getItem(int position) 的返回类型与实际返回的值是否一致是否存在返回下标本身 3. getItemId(int position) 的返回值在数据增删排序后是否稳定hasStableIds() 的取值是否与之匹配 4. getView() 里对 getItem(position) 的使用是否与 getItem 的返回类型对齐 5. 只给出需要修改的方法和最小改动不要重写整个类不要改动布局文件。这段提示词的关键是最小改动和不要重写整个类。不给约束的话模型很容易把整个getView()换成RecyclerView.Adapter的写法——那不是你想要的答案ListView和RecyclerView的适配器接口本来就不是一套。4.2 错法一getItem 返回下标// 错误写法 Override public Object getItem(int position) { return position; }// 正确写法 Override public Note getItem(int position) { return data.get(position); }改完之后getView()里的Note item getItem(position);不再需要强转ClassCastException也就没了。顺带说一句把返回类型从Object改成Note是协变返回类型Java 允许IDE 也不会警告。4.3 错法二getItemId 一律 return 0// 错误写法BaseAdapter 的默认实现很多人忘了重写 Override public long getItemId(int position) { return 0; }// 正确写法数据有稳定主键时 Override public long getItemId(int position) { return getItem(position).getId(); } Override public boolean hasStableIds() { return true; }如果你暂时没有稳定主键另一种合理写法是保持hasStableIds()为falsegetItemId()返回position并在注释里写清楚这个 ID 只在当前数据集快照内有效。4.4 错法三getCount 写死或与数据源不同步// 错误写法 Override public int getCount() { return 1; }// 正确写法 Override public int getCount() { return data null ? 0 : data.size(); }判断数据源不同步有个很直接的办法在getCount()里临时打一行日志然后调用notifyDataSetChanged()对比日志里的数值和data.size()。数值对不上就是适配器持有的集合和界面刷新用的集合不是同一个对象引用——这种问题在data newList;之后忘记刷新时特别常见。5. 验证notifyDataSetChanged 之后 position 到 id 还对不对5.1 本地打日志核对映射改完这三个方法别急着交付。加一段只在 debug 构建里跑的自检代码把position → getItem(position) → getItemId(position)的映射打出来Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); if (BuildConfig.DEBUG) { for (int i 0; i getCount(); i) { Log.d(AdapterCheck, pos i item getItem(i) id getItemId(i) stable hasStableIds()); } } }然后在本地 Android Studio 编译、装到测试机、用adb logcat -s AdapterCheck采集输出把日志贴回 Codex 的对话里让它判断id列是否有重复、item列是否为null。编译、运行、抓日志这三步只能你在本地做AI 编程工具没有你的设备权限也不应该去连你的构建机。5.2 点击与选中态回归日志对得上之后做两个手工回归第一点击第 N 行确认onItemClick里的position和id都指向同一条数据第二滚动到列表中部删除一条数据再notifyDataSetChanged()观察选中态有没有跳到别的行。hasStableIds()返回true但getItemId()不稳定的时候第二个回归几乎必挂这也是分不清 getItemId最真实的代价。6. 排障401、404 与 ClassCastException 分属两层6.1 Codex 这一侧的错误长什么样报 401先看TAOTOKEN_API_KEY有没有真的进到 Codex 进程的环境里——IDEA 或者终端重启之后环境变量会丢最省事的做法是在启动 Codex 的那个 shell 里echo $TAOTOKEN_API_KEY确认一下。报 404八成是base_url多写了/v1或者不小心把带?utm_source的官网落地页填了进去base_url只写https://taotoken.net/api。还有一种报错是模型不存在那是对照model字段和模型广场列表不一致导致的。6.2 Adapter 这一侧的错误长什么样ClassCastException: java.lang.Integer cannot be cast to ...对应getItem()返回了下标。IndexOutOfBoundsException: Invalid index N, size is M通常对应getCount()的返回值大于实际数据长度也就是适配器里的集合和渲染用的集合不是同一个引用。onItemClick的id恒为 0 而不抛任何异常对应getItemId()没有重写。三层错误混在一个 logcat 里时先分层再看堆栈里出现okhttp、Retrofit这类网络库的是 Codex 配置层出现ListView、AdapterView、你自己的包名的是 Adapter 实现层。分清楚之后再回去看第 3 节那张对照表基本一次就能定位。7. 配通 Codex 之后回控制台确认这次的调用一路排查下来你会往对话里贴不少代码和日志。这些调用有没有正常记上账去控制台看一眼就清楚了打开 TaoToken 模型对话 用同一把 Key 发一条测试消息确认 Base URL 和模型 ID 没填错如果准备长期用它读代码、对签名可以去 Coding Plan 看套餐够不够用要再建一把给别的项目用的 Key在 控制台 API Keys 页面创建。三个页面用的是同一套账号体系切换过去不需要重新注册。最后留一句实在话getCount()、getItem()、getItemId()这三个方法之所以让人反复翻文档不是因为难而是因为它们的默认实现和调用方都在框架内部写错了不报错。把 Codex 接上之后真正省下来的时间不是让 AI 写代码而是你贴一次代码就能拿到一份逐条对照的清单——省下的是翻 AOSP 源码和 StackOverflow 的那半小时。Adapter 里的每个return写什么还是你自己说了算。