1. 从 ExpandableListActivity 说起:老控件学习场景里的新需求
Android 里的ExpandableListActivity是个挺有年代感的控件基类,它继承自Activity,内部帮你把ExpandableListView和ExpandableListAdapter的绑定逻辑都封装好了。你只要继承它、在onCreate里调一次setListAdapter,一个可展开的二级列表就出来了。父级点一下展开,子级再点一下响应事件,做通讯录分组、设置项分类、商品类目这种场景特别顺手。
但我在照着老教程敲示例的时候,总会遇到一个很现实的问题:学习笔记写到一半,想顺手接个 AI 能力做点小实验,比如把列表项内容丢给模型做摘要、或者让模型帮我生成测试数据,结果卡在配置上。要么是每个工具各配一套 Key,要么是环境变量、配置文件散落各处,笔记还没写完,配置先乱了。这时候如果有一个统一的 Key 和 API 通道,把配置收敛到一个settings.json里,学习节奏就不会被打断。
这篇笔记就是围绕这个场景来的:一边把ExpandableListActivity的三种典型绑定方式(SimpleExpandableListAdapter、SimpleCursorTreeAdapter、BaseExpandableListAdapter)过一遍,一边把 TaoToken 的统一 Key 接入配置骨架搭好,让你在写学习笔记的同时,能跑通一次真实请求,确认配置生效。适合正在补 Android 基础控件、又想顺手把 AI 工具链接入流程走通的开发者。
2. TaoToken 前置准备:统一 Key 与 API 通道
TaoToken 在这里扮演的角色,是一个统一的模型调用入口。你不用为每个模型或工具单独维护一套鉴权信息,而是拿一个 Key,通过统一的 API 地址去请求。对学习笔记这种场景来说,好处很直接:配置项少、迁移成本低、写进settings.json里一目了然。
你需要先拿到自己的 API Key。入口在控制台的 API Keys 页面,登录后创建一个即可。创建完记得复制保存,页面上通常只完整显示一次。
- 控制台入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
API 的基础地址是https://taotoken.net/api,注意这个地址后面不加任何查询参数,保持干净。请求时把 Key 放在Authorization头里,格式是Bearer <你的Key>。这套约定和主流 OpenAI 兼容接口一致,所以你在 Android 侧用 OkHttp、Retrofit 或者干脆用 curl 验证,写法都很熟悉。
注意:Key 属于敏感信息,别硬编码进提交到 Git 的源码里。学习阶段可以放在本地
settings.json并加入.gitignore,正式项目建议走环境变量或密钥管理服务。
3. 可复制的 settings.json 配置骨架
下面这份settings.json是我在学习笔记里用的骨架,把 TaoToken 的接入信息集中管理。你可以直接复制,把apiKey换成自己的。
{ "taotoken": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-替换成你自己的Key", "defaultModel": "claude-3-5-sonnet", "timeoutMs": 30000, "headers": { "Content-Type": "application/json" } }, "android": { "expandableList": { "groupLayout": "android.R.layout.simple_expandable_list_item_2", "childLayout": "android.R.layout.simple_expandable_list_item_2", "groupFrom": ["NAME", "IS_EVEN"], "groupTo": ["android.R.id.text1", "android.R.id.text2"] } } }几个字段说明一下。baseUrl固定用https://taotoken.net/api,不要在后面拼/v1之类的路径,具体端点由请求时补全。defaultModel填你打算默认调用的模型名,学习阶段随便选一个能用的就行。timeoutMs给 30 秒,模型请求比普通接口慢,别设太短。headers里只放通用头,鉴权头在代码里动态拼,避免 Key 出现在静态配置的多个副本里。
如果你在 Android 工程里读取这份配置,可以放在assets/settings.json,用AssetManager读进来解析。这样配置和代码分离,改 Key 不用重新编译逻辑代码。
fun loadSettings(context: Context): JSONObject { val text = context.assets.open("settings.json") .bufferedReader().use { it.readText() } return JSONObject(text) }读出来之后,taotoken.apiKey和taotoken.baseUrl就是后续请求要用的两个核心值。
4. 最小验证:跑通一次请求并确认配置生效
配置搭好了,得验证它真的能用。最直接的方式是先脱离 Android,用命令行确认 Key 和地址没问题,再回到工程里接。
先看命令行验证。把下面的YOUR_KEY换成你的 Key,直接执行:
curl -X POST https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-5-sonnet", "messages": [ {"role": "user", "content": "用一句话说明 ExpandableListActivity 的作用"} ] }'如果返回里带有choices字段和模型输出内容,说明 Key、地址、模型名三者都对上了。这一步过了,再往 Android 里搬。
Android 侧用 OkHttp 发同样的请求,核心代码如下:
val client = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() val settings = loadSettings(context) val apiKey = settings.getJSONObject("taotoken").getString("apiKey") val baseUrl = settings.getJSONObject("taotoken").getString("baseUrl") val body = JSONObject().apply { put("model", "claude-3-5-sonnet") put("messages", JSONArray().put(JSONObject().apply { put("role", "user") put("content", "用一句话说明 ExpandableListActivity 的作用") })) }.toString() val request = Request.Builder() .url("$baseUrl/chat/completions") .addHeader("Authorization", "Bearer $apiKey") .addHeader("Content-Type", "application/json") .post(body.toRequestBody("application/json".toMediaType())) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Log.e("TaoToken", "请求失败: ${e.message}") } override fun onResponse(call: Call, response: Response) { val result = response.body?.string() Log.d("TaoToken", "返回: $result") } })跑起来后看 Logcat,如果打印出模型返回的文本,配置就生效了。这时候你可以把这段请求逻辑挂到ExpandableListActivity的某个子项点击事件上,比如点开某个分组后,把子项内容发给模型做处理,学习笔记的示例就活了。
5. 本篇常见错排查
配置和请求过程中,有几个坑我踩过,列出来帮你省时间。
第一个是 401 未授权。多半是 Key 拼错、复制时带了空格,或者Authorization头里忘了Bearer前缀。检查一下头部的完整格式,Bearer和 Key 之间是一个空格。
第二个是 404 路径错误。常见原因是把baseUrl写成了https://taotoken.net/api/v1,然后请求时又拼了/chat/completions,变成/api/v1/chat/completions。按本文约定,baseUrl保持https://taotoken.net/api,端点直接拼/chat/completions。
第三个是超时。模型请求比普通接口慢,默认超时太短会直接失败。把连接和读取超时都设到 30 秒以上,别用默认值。
第四个是 Android 主线程网络异常。NetworkOnMainThreadException说明你把请求放在了主线程,用enqueue异步或者切到子线程即可。
第五个是settings.json读取失败。确认文件放在assets目录下,且AssetManager的路径没有多余前缀。解析时用 try-catch 包一下,打印原始文本方便定位。
提示:如果请求返回 200 但内容为空,检查
model字段是不是写了一个不存在的模型名。换一个文档里列出的模型再试。
6. 后续怎么用:从学习笔记到长期编码
验证跑通之后,这份配置骨架就能复用了。如果你只是偶尔在学习笔记里调一下模型,用 API Keys 加接入文档的方式最轻量,随用随配。文档地址在这里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
如果你想在模型对话里直接对比不同模型的输出效果,可以走模型对话入口,省去自己写请求的功夫:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
要是你打算把 AI 能力长期接进编码流程,比如让 Agent 帮你补全ExpandableListAdapter的样板代码、生成测试数据,那 Coding Plan 更合适,配置一次就能持续用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
回到ExpandableListActivity本身,三种适配器各有适用场景。SimpleExpandableListAdapter适合静态的List<Map>数据,写示例最快;SimpleCursorTreeAdapter适合数据来自数据库游标的场景,父子查询分离;BaseExpandableListAdapter最灵活,视图完全自定义,适合复杂布局。你可以把本文的请求逻辑分别挂到这三种适配器的点击回调里,对比一下哪种更适合你的笔记结构。配置这件事,一次搭好,后面就只剩写业务逻辑了。