1. ListView 点击事件三种写法与 401 调试场景
Android ListView 点击事件三种方式,是很多做安卓开发的朋友绕不开的基础功:AdapterView.OnItemClickListener 外部监听、Adapter 内部 getView 绑定、Activity 实现接口。这三种写法本身不难,难的是点击之后触发的网络请求在本地调试时突然返回 401,日志里只有一行HTTP 401 Unauthorized,你分不清是 Key 没带上、Header 拼错了,还是 endpoint 根本打到了错误的地址。这篇就把这两件事串起来讲:先把三种点击事件的可复制代码写清楚,再把请求链路统一收口到 TaoToken 的 Key 与 API 通道上,用日志对照的方式定位 401。
适合谁看:刚接触 ListView 的安卓初学者、正在把本地接口切到统一网关的移动端开发者、以及被 401 卡住但不想盲改代码的人。核心检索词就是 ListView 点击事件与 401 排查,全文围绕这两条线展开。
先说清楚三种方式的本质差别,后面调试才不会乱。它们最终都调用parent.getItemAtPosition(position),但返回值类型完全不同:BaseAdapter 返回你自定义的实体对象,SimpleAdapter 返回Map<String,Object>,SimpleCursorAdapter 返回Cursor。很多人 401 排查到一半发现点击回调压根没进,就是因为强转类型写错导致运行时崩溃,日志被异常刷掉,误以为是网络问题。
我试过在同一个 Demo 里把三种方式都实现一遍,然后统一在点击回调里发一个请求,这样对比起来最直观。下面按「先写点击、再接网络、再排 401」的顺序走,每一步都给可复制的片段。
2. TaoToken 前置准备:统一 Key 与 API 通道
在动手改 endpoint 之前,先把 TaoToken 这一层准备好。它的作用是给你的请求提供一个统一的 Key 和 API 通道,这样本地调试时不用在多个接口地址之间来回切换,401 的成因也能收敛到「Key 是否正确、Header 是否规范」这两点上。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api (这个不加 UTM)。
你需要拿到两样东西:一个 API Key,以及确认要调用的 Model ID。Key 在控制台的 API Keys 页面创建,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys_cta&utm_campaign=rewrite 。创建后立刻复制保存,页面刷新后通常不再完整显示。Model ID 则在你打算调用的模型说明里,比如对话类、编码类各有对应的标识,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc_cta&utm_campaign=rewrite 。
这里有个容易踩的坑:很多人把 Key 直接硬编码进MainActivity,然后 git 提交上去,回头 Key 泄露了又得重新建。建议放在local.properties或BuildConfig里,通过 Gradle 注入。下面是一个build.gradle(app 模块)的片段,把 Key 和 Base URL 都做成可配置项:
android { defaultConfig { buildConfigField "String", "TAOTOKEN_BASE_URL", "\"https://taotoken.net/api\"" buildConfigField "String", "TAOTOKEN_API_KEY", "\"${project.findProperty("TAOTOKEN_API_KEY") ?: ""}\"" buildConfigField "String", "TAOTOKEN_MODEL_ID", "\"你的模型ID\"" } }然后在gradle.properties(不要提交到仓库)里写:
TAOTOKEN_API_KEY=sk-你的真实Key这样代码里用BuildConfig.TAOTOKEN_API_KEY就能取到,切换环境只改一处。如果你用的是 Claude Code 这类编码工具做辅助开发,接入方式在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode_cta&utm_campaign=rewrite ,思路是一样的:Base URL、Key、Model ID 三件套齐全,缺一个就会 401 或 404。
前置准备做完,你手上应该有三样确定的东西:Base URL 是https://taotoken.net/api,一个可用的 Key,一个明确的 Model ID。接下来才进入点击事件和请求代码。
3. 三种点击事件的可复制配置与请求收口
这一节是全文的技术主体。三种点击事件分别给完整片段,然后在每种方式的回调里统一调用一个sendRequest()方法,把 endpoint 指向 TaoToken。这样点击行为和网络请求解耦,401 排查时只看sendRequest()一处即可。
3.1 方式一:AdapterView.OnItemClickListener 外部监听
这是最常见的写法,在 Activity 或 Fragment 里给 ListView 设置监听器。先定义实体和 Adapter:
public class Person { private String name; private int balance; public Person(String name, int balance) { this.name = name; this.balance = balance; } public String getName() { return name; } public int getBalance() { return balance; } }BaseAdapter 的getView正常写,这里不展开。关键是监听器:
personLV.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Person p = (Person) parent.getItemAtPosition(position); Toast.makeText(getApplicationContext(), p.getName(), Toast.LENGTH_SHORT).show(); sendRequest(p.getName()); } });注意parent.getItemAtPosition(position)返回的是Object,这里强转成Person。如果你 Adapter 的getItem返回的不是 Person,这行会抛ClassCastException,日志里看不到 401,只看到崩溃。这是排查顺序上的第一个分叉点。
3.2 方式二:Adapter 内部 getView 绑定点击
这种方式把点击逻辑写进 Adapter 的getView,适合每个 item 点击行为不一致的场景。片段如下:
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_person, parent, false); holder = new ViewHolder(); holder.nameTv = convertView.findViewById(R.id.tv_name); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } Person p = getItem(position); holder.nameTv.setText(p.getName()); convertView.setOnClickListener(v -> { Toast.makeText(v.getContext(), p.getName(), Toast.LENGTH_SHORT).show(); sendRequest(p.getName()); }); return convertView; }这里有个细节:convertView.setOnClickListener每次getView都会重新设置,复用机制下不会错位,因为闭包捕获的是当前position对应的p。但如果你在getView里做异步请求且没做防抖,快速滑动会触发多次请求,日志里出现重复的 401,容易误判成 Key 失效。
3.3 方式三:Activity 实现 OnItemClickListener 接口
这种方式让 Activity 本身实现接口,代码更集中:
public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView personLV = findViewById(R.id.lv_person); personLV.setAdapter(new PersonAdapter(this, dataList)); personLV.setOnItemClickListener(this); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Person p = (Person) parent.getItemAtPosition(position); Toast.makeText(getApplicationContext(), p.getName(), Toast.LENGTH_SHORT).show(); sendRequest(p.getName()); } }三种方式到这里都齐了。它们的差别确实就在getItemAtPosition的返回值:方式一和方式三拿到的是实体,方式二在 Adapter 内部直接getItem(position)。如果你用的是 SimpleAdapter,返回值是Map,取值要写map.get("balance");用 SimpleCursorAdapter 则是Cursor,要写c.getString(1)。类型对不上,点击回调就进不去,更别提 401 了。
3.4 统一请求方法:endpoint 指向 TaoToken
三种点击最终都调sendRequest()。用 OkHttp 写一个最小实现:
private final OkHttpClient client = new OkHttpClient(); private void sendRequest(String keyword) { MediaType JSON = MediaType.parse("application/json; charset=utf-8"); String body = "{\"model\":\"" + BuildConfig.TAOTOKEN_MODEL_ID + "\"," + "\"messages\":[{\"role\":\"user\",\"content\":\"" + keyword + "\"}]}"; Request request = new Request.Builder() .url(BuildConfig.TAOTOKEN_BASE_URL + "/v1/chat/completions") .addHeader("Authorization", "Bearer " + BuildConfig.TAOTOKEN_API_KEY) .addHeader("Content-Type", "application/json") .post(RequestBody.create(body, JSON)) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e("TAOTOKEN", "request failed: " + e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { String resp = response.body() != null ? response.body().string() : ""; Log.d("TAOTOKEN", "code=" + response.code() + " body=" + resp); } }); }这里 Base URL 和 Key 都来自BuildConfig,Model ID 也是。三件套齐全,请求才会被正确路由。如果你把 Base URL 写成https://taotoken.net/api/(末尾多斜杠)再拼/v1/...,可能出现双斜杠,部分网关会返回 404 而不是 401,日志要看清 code。
4. 验证请求与成功结果对照
代码写完,先别急着点 item。用 curl 在命令行验证一次,确认 Key 和 endpoint 本身没问题,再去跑 App。这样能把「网络层问题」和「UI 层问题」分开。
curl -X POST "https://taotoken.net/api/v1/chat/completions" \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "你的模型ID", "messages": [{"role": "user", "content": "ping"}] }'成功时你会看到类似{"choices":[{"message":{"role":"assistant","content":"..."}}]}的返回,HTTP 状态码 200。如果这里就 401,说明 Key 或 Header 有问题,跟 ListView 无关,先解决这一层。
命令行通了之后跑 App,点击任意 item,Logcat 过滤TAOTOKEN标签。正常日志长这样:
D/TAOTOKEN: code=200 body={"choices":[{"message":{"role":"assistant","content":"..."}}]}对照关系很清晰:curl 200 + App 200,链路通;curl 200 + App 401,问题在 App 的 Header 或 Key 注入;curl 401 + App 401,问题在 Key 本身或账户状态。这个对照表能帮你快速缩小范围。
如果你更想先在网页上验证模型是否可用,可以直接用模型对话页面发一条消息,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models_cta&utm_campaign=rewrite 。网页能通、curl 能通、App 不通,那基本锁定在客户端代码。
5. 本篇常见错误排查
这一节按真实报错来对。401 只是表象,下面几种情况都会表现为请求失败,日志关键词不同,处理方式也不同。
第一种,HTTP 401 Unauthorized,body 里常带invalid api key或missing authorization。原因通常是 Key 没注入成功,BuildConfig.TAOTOKEN_API_KEY是空字符串,Header 变成Bearer。检查gradle.properties是否被读取,以及是否在正确的 build variant 下。三件套里 Key 缺失是最常见的。
第二种,local proxy failed或连接被拒绝。这类多半是本地网络环境或客户端代理配置导致请求没出去,跟 Key 无关。先确认 curl 在同一台机器上能否通,能通就说明是 App 侧的网络配置问题,检查是否误设了代理。
第三种,日志里出现reading choices相关解析异常,比如Expected BEGIN_OBJECT but was STRING。这通常是你用 Gson 解析返回体,但返回的其实是错误结构(比如 401 的 JSON 里没有choices字段)。先打印原始 body,再决定解析逻辑,别直接fromJson。
第四种,OAuth 或鉴权流程相关报错,比如OAuth token expired。如果你用的是带 OAuth 的接入方式,Key 和 token 是两回事,过期要重新获取。普通 API Key 方式不会出现这个。
第五种,点击没反应,日志里连TAOTOKEN都没有。回到第 3 节,检查getItemAtPosition的强转类型是否匹配你的 Adapter。类型错了会抛异常,回调根本没执行到sendRequest()。
排查顺序建议固定下来:先看点击回调有没有进(有没有 Toast),再看sendRequest有没有被调用(有没有 request 日志),再看 HTTP code,最后看 body。这个顺序能避免你在 UI 层和网络层之间反复横跳。
6. 把调试链路固定下来的做法
三种点击事件写完之后,真正省时间的做法是把请求收口到一个方法、把配置收口到BuildConfig、把验证收口到 curl 加 Logcat 对照。这样下次再遇到 401,你不需要重新读一遍 Adapter 代码,直接按「curl 通不通 → App 日志 code → body 内容」三步走。
如果你后续要做的是长期编码或 Agent 类任务,可以把 Key 和通道的管理放到 Coding Plan 里统一处理,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=codingplan_cta&utm_campaign=rewrite ,思路和这篇一致:Base URL、Key、Model ID 三件套先对齐,再谈业务逻辑。日常需要快速验证模型返回时,模型对话页面和 API Keys 页面配合使用就够了。把这篇的sendRequest()留着,换成你自己的业务参数,点击事件的三种写法不用再重写。