资讯详情 RecyclerView使用详解(三):把 Adapter 的 Base URL 改到 TaoToken 做多模型联调
📅 2026/10/12 15:48:25
1. 从假数据到真接口RecyclerView 列表联调的真实痛点RecyclerView 的 Adapter 写起来不难难的是数据源从本地假数据切到真实接口那一刻。我见过太多项目卡在这一步本地ArrayListString跑得好好的一换成网络请求列表要么空白要么滚动时疯狂闪烁要么报一堆看不懂的错。尤其是现在很多团队想让列表项内容由大模型动态生成比如做一个「AI 灵感卡片流」「多模型回答对比列表」Adapter 里塞的不再是固定文案而是每次请求返回的模型输出这时候联调复杂度直接翻倍。这篇聚焦一个具体场景RecyclerView 列表数据源从本地假数据切换到真实大模型接口以 Adapter 为切入点把请求 endpoint 与鉴权配置改到 TaoToken 统一通道让列表项内容由模型动态生成。适合已经会写基础 Adapter、但没跑通「列表 大模型接口」联调的 Android 开发者。核心检索词就是 RecyclerView Adapter 多模型联调读完你能拿到可直接复制的 Adapter 与网络层配置片段以及列表滚动加载、错误重试的验证步骤。先说清楚 TaoToken 在这里扮演什么角色。它是一个统一的大模型调用通道你不需要为每个模型单独申请 Key、单独改 Base URL。官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。对 Android 端来说你只需要把 Retrofit 或 OkHttp 的 baseUrl 指向它把 Key 放进请求头剩下的模型切换在请求体里改 model 字段就行。这对 RecyclerView 这种「一个列表要展示多个模型结果」的场景特别友好因为 Adapter 不用关心底层是哪个模型它只负责把返回的文本绑到 TextView 上。为什么强调 Adapter 是切入点因为 RecyclerView 的刷新机制和普通 View 不一样。你调用notifyDataSetChanged()会触发整表重绘滚动位置可能丢失用notifyItemInserted()才能保留动画和滚动状态。当数据来自异步接口时回调线程和主线程的切换、列表 diff 的计算、加载更多时的分页状态全都要在 Adapter 和它的数据仓库之间协调好。很多人第一次联调失败不是接口不通而是 Adapter 的刷新时机错了。我试过最稳的做法是Adapter 只持有ListUiModel所有网络请求和状态管理放在 ViewModel 或一个 Repository 里通过 LiveData 或 Flow 把结果推给 Adapter。这样 Adapter 保持纯粹联调时你只需要盯住「数据有没有到」「到了之后有没有正确 notify」两件事。下面从环境准备开始一步步把这条链路搭起来。2. TaoToken 前置准备Base URL、Key 与 Model ID 三件套在动 Adapter 之前先把调用通道配好。TaoToken 的接入需要三样东西Base URL、API Key、Model ID。这三件套缺一不可而且必须和你的网络层配置完全一致否则后面报 401 或 404 你会查很久。Base URL 统一用https://taotoken.net/api注意这里不加任何 UTM 参数UTM 只用于官网跳转统计。API Key 需要你去控制台创建入口是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建后复制那串以sk-开头的字符串。Model ID 取决于你想调哪个模型比如做多模型对比列表时你可以在请求体里分别传不同的 model 值Adapter 侧只需要给每个 item 标记它属于哪个模型即可。如果你用的是 Claude Code 这类编码工具做联调辅助它的配置也是同一套逻辑Base URL 填https://taotoken.net/apiKey 填你的 API KeyModel ID 填你选的模型。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的 settings 片段。不过这篇主要讲 Android 端工具配置只是顺带提一句避免你混淆。Android 项目里我建议把这三件套放在local.properties或 BuildConfig 里不要硬编码在 Java/Kotlin 文件里。示例# local.properties TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_MODEL_ID你的模型ID然后在build.gradle里读取并注入 BuildConfigandroid { defaultConfig { buildConfigField String, TAOTOKEN_BASE_URL, \${localProperties[TAOTOKEN_BASE_URL]}\ buildConfigField String, TAOTOKEN_API_KEY, \${localProperties[TAOTOKEN_API_KEY]}\ buildConfigField String, TAOTOKEN_MODEL_ID, \${localProperties[TAOTOKEN_MODEL_ID]}\ } }这样切换环境时只改一个文件Adapter 和网络层都不用动。注意 Key 不要提交到 Gitlocal.properties默认就在.gitignore里。如果你需要长期跑编码类 Agent 任务比如让列表项内容由模型持续生成并做多轮对话可以考虑 Coding Plan入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它适合那种「列表不断追加模型输出」的长任务场景比单次请求更省心。但如果你只是做一次性的列表联调用普通 API Key 就够了。配好三件套后先别急着写 Adapter。用 curl 或 Postman 发一个最小请求确认通道是通的curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: 你的模型ID, messages: [{role: user, content: 用一句话介绍 RecyclerView}] }如果返回里有choices[0].message.content说明通道没问题可以进入 Android 侧配置。如果返回 401检查 Key 有没有多余空格如果返回 404检查 Base URL 是不是写成了带/v1的完整路径导致重复。这些坑后面第五节会详细对照。3. 可复制配置Retrofit Adapter 的完整片段这一节给可直接复制的代码。先配网络层用 Retrofit OkHttp Gson这是 Android 里最通用的组合。注意 Base URL 必须以/结尾否则 Retrofit 会拼错路径。// ApiClient.kt import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit object ApiClient { private const val BASE_URL https://taotoken.net/api/ private val okHttp OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .addInterceptor { chain - val request chain.request().newBuilder() .addHeader(Authorization, Bearer ${BuildConfig.TAOTOKEN_API_KEY}) .addHeader(Content-Type, application/json) .build() chain.proceed(request) } .addInterceptor(HttpLoggingInterceptor().apply { level HttpLoggingInterceptor.Level.BODY }) .build() val service: ChatService by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttp) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ChatService::class.java) } }接口定义和请求体// ChatService.kt import retrofit2.http.Body import retrofit2.http.POST interface ChatService { POST(v1/chat/completions) suspend fun chat(Body body: ChatRequest): ChatResponse } data class ChatRequest( val model: String, val messages: ListMessage, val stream: Boolean false ) data class Message(val role: String, val content: String) data class ChatResponse(val choices: ListChoice) data class Choice(val message: Message)Adapter 侧我建议用一个带状态的 UiModel而不是直接绑 String。这样加载中、成功、失败三种状态都能在同一个列表里表达// ChatItemAdapter.kt import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView sealed class ChatItem { data class Loading(val id: String) : ChatItem() data class Success(val id: String, val model: String, val content: String) : ChatItem() data class Error(val id: String, val msg: String) : ChatItem() } class ChatItemAdapter( private val onRetry: (String) - Unit ) : ListAdapterChatItem, ChatItemAdapter.VH(DIFF) { companion object { private val DIFF object : DiffUtil.ItemCallbackChatItem() { override fun areItemsTheSame(a: ChatItem, b: ChatItem) a.id b.id override fun areContentsTheSame(a: ChatItem, b: ChatItem) a b } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_chat, parent, false) return VH(view) } override fun onBindViewHolder(holder: VH, position: Int) { holder.bind(getItem(position), onRetry) } class VH(itemView: View) : RecyclerView.ViewHolder(itemView) { private val tvModel: TextView itemView.findViewById(R.id.tv_model) private val tvContent: TextView itemView.findViewById(R.id.tv_content) private val progress: ProgressBar itemView.findViewById(R.id.progress) fun bind(item: ChatItem, onRetry: (String) - Unit) { when (item) { is ChatItem.Loading - { progress.visibility View.VISIBLE tvModel.text 生成中... tvContent.text } is ChatItem.Success - { progress.visibility View.GONE tvModel.text item.model tvContent.text item.content } is ChatItem.Error - { progress.visibility View.GONE tvModel.text 请求失败 tvContent.text item.msg itemView.setOnClickListener { onRetry(item.id) } } } } } }注意这里用的是ListAdapter而不是裸RecyclerView.Adapter因为它内置了 DiffUtil调用submitList()时自动算差异滚动位置和动画都能保留。这是从假数据切到真接口时最值得改的一处以前你可能直接notifyDataSetChanged()现在改成submitList(newList)列表不会闪。ViewModel 里发起请求并组装列表// ChatViewModel.kt import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class ChatViewModel : ViewModel() { private val _items MutableStateFlowListChatItem(emptyList()) val items: StateFlowListChatItem _items fun loadModels(prompts: ListPairString, String) { viewModelScope.launch { val current _items.value.toMutableList() prompts.forEach { (id, prompt) - current.add(ChatItem.Loading(id)) } _items.value current prompts.forEach { (id, prompt) - try { val resp ApiClient.service.chat( ChatRequest( model BuildConfig.TAOTOKEN_MODEL_ID, messages listOf(Message(user, prompt)) ) ) val text resp.choices.firstOrNull()?.message?.content ?: 空响应 updateItem(id, ChatItem.Success(id, BuildConfig.TAOTOKEN_MODEL_ID, text)) } catch (e: Exception) { updateItem(id, ChatItem.Error(id, e.message ?: 未知错误)) } } } } private fun updateItem(id: String, newItem: ChatItem) { val list _items.value.toMutableList() val idx list.indexOfFirst { it.id id } if (idx 0) { list[idx] newItem _items.value list } } }Activity 里把两者接起来// MainActivity.kt class MainActivity : AppCompatActivity() { private lateinit var adapter: ChatItemAdapter private val vm: ChatViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) adapter ChatItemAdapter { id - vm.loadModels(listOf(id to 重试请再生成一次)) } findViewByIdRecyclerView(R.id.recycler).apply { layoutManager LinearLayoutManager(thisMainActivity) adapter thisMainActivity.adapter } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { vm.items.collect { adapter.submitList(it) } } } vm.loadModels(listOf( m1 to 用一句话解释什么是协程, m2 to 用一句话解释什么是 Flow )) } }这套配置里Base URL、Key、Model ID 三件套全部来自 BuildConfigAdapter 完全不碰网络细节。你要切模型只改TAOTOKEN_MODEL_ID要加新模型对比在loadModels的 prompts 里多传一组就行。4. 验证请求与成功结果滚动加载、重试与日志确认配好之后怎么确认真的跑通了分三步验证。第一步看 Logcat 里的 OkHttp 日志。因为加了HttpLoggingInterceptor.Level.BODY你会看到完整的请求 URL、请求头和响应体。重点确认三处URL 是不是https://taotoken.net/api/v1/chat/completionsAuthorization 头是不是Bearer sk-...响应体里有没有choices数组。如果 URL 少了/v1或多了重复的/api说明 Base URL 拼接有问题。第二步看列表渲染。启动后应该先看到两个 Loading 状态的 item进度条转圈然后陆续变成 Success显示模型返回的文本。如果一直停在 Loading说明请求没回来或回调没触发如果直接变 Error看错误信息是什么。成功时列表项应该显示模型名和生成内容滚动流畅没有闪烁。第三步验证滚动加载和重试。滚动加载的做法是在 Adapter 里监听最后一个可见项触发加载更多recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) { val lm rv.layoutManager as LinearLayoutManager val last lm.findLastVisibleItemPosition() if (last adapter.itemCount - 1) { // 触发加载下一页注意加防抖 vm.loadMoreIfNeeded() } } })防抖很重要否则一次滚动会触发几十次请求。可以在 ViewModel 里用一个isLoading标志位挡住重复调用。重试的验证把TAOTOKEN_API_KEY故意改错一位重启 App列表应该出现 Error 状态点击该 item 触发onRetry把 Key 改回来后再次点击应该能变成 Success。这个过程能验证你的错误分支和重试链路是通的。成功结果的判断标准很简单Logcat 里看到 200 响应列表里看到模型生成的文本滚动时不卡顿点击错误项能重试。三条都满足说明 RecyclerView 列表数据源已经成功从本地假数据切到了 TaoToken 的真实接口。如果你还想在浏览器里直接对比不同模型的输出可以用模型对话入口 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 先在网页上确认某个模型返回正常再回到 Android 端联调能省不少排查时间。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth联调时最容易撞上的几类报错这里逐个对照。401 Unauthorized。最常见的原因是 Key 错了或没带上。检查三点BuildConfig 里读到的 Key 是不是空字符串local.properties没配或没同步请求头是不是Authorization: Bearer sk-xxx注意 Bearer 后面有一个空格Key 有没有被换行符污染。如果用的是 Claude Code 或 Codex 这类工具401 还可能是auth.json里的 Key 过期需要重新生成。Codex 的auth.json里通常包含OPENAI_API_KEY字段改成你的 TaoToken Key 即可同时确认 Base URL 指向https://taotoken.net/api。local proxy failed。这个报错通常出现在你本地配了代理工具但代理没启动或端口不对。Android 模拟器访问宿主机代理需要特殊地址10.0.2.2真机则要保证手机和电脑同一网段。最省事的做法是联调阶段先关掉本地代理直接用 TaoToken 的地址确认通道本身是通的。如果必须走代理检查 OkHttp 有没有配proxy()以及代理端口是否和工具里一致。reading choices 相关报错。典型的是Expected BEGIN_ARRAY but was BEGIN_OBJECT或reading choices失败。这几乎都是响应结构和你的数据类不匹配。TaoToken 返回的是标准 chat completions 结构choices是数组每个元素有message.content。如果你的ChatResponse写成了choices: Choice而不是ListChoiceGson 解析就会报这个错。对照第三节的数据类检查一遍。另外如果模型返回的是流式streamtrue响应体是 SSE 格式不是标准 JSON用 Gson 直接解析也会失败。联调阶段先用streamfalse。OAuth 相关报错。如果你在 Claude Code 或类似工具里看到 OAuth 失败通常是因为工具默认走官方登录流程而你要用的是 API Key 模式。需要在配置里显式指定 API Key 和 Base URL关掉 OAuth 登录。Claude Code 的配置文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 settings 片段把 Base URL、Key、Model ID 三件套填全即可。CC Switch 这类切换工具也是同样逻辑三件套缺一不可。还有一个隐蔽的坑RecyclerView 的submitList传入的 List 如果是同一个引用DiffUtil 会认为没变化列表不刷新。解决办法是每次传新构造的 List比如list.toList()或ArrayList(list)。这个坑在假数据阶段不会遇到切到接口后很容易踩。6. 把联调链路固化下来下一步怎么走走到这里你的 RecyclerView 应该已经能展示 TaoToken 返回的模型内容了。接下来值得做的几件事把 Adapter 的错误状态和重试做成通用组件这样换任何接口都能复用把 Model ID 做成可配置项在设置页里让用户切换列表项上标注是哪个模型生成的给滚动加载加上分页游标避免重复请求。如果你要长期跑多模型对比或 Agent 类任务Coding Plan 比单次 API Key 更适合入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。API Key 的管理在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。需要新建 Key 时走 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。最后留一个实用技巧联调阶段把 OkHttp 的日志级别开到 BODY但上线前记得关掉或降到 BASIC否则请求体和响应体里的内容会打到日志里既不安全也影响性能。这个开关放在 BuildConfig 里按 debug/release 区分最省事。