多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Android 适配器教程(一):从 ListView 到 ArrayAdapter 的 TaoToken 实践

Android 适配器教程(一):从 ListView 到 ArrayAdapter 的 TaoToken 实践 1. 为什么你的 ListView 总是空白从适配器职责说起如果你刚开始写 Android大概率会遇到这样一个场景布局里放了一个 ListView代码里也塞了数据运行起来却是一片空白或者只有一行字。这个问题十有八九出在适配器上。ListView 本身不存数据它只负责“画”真正决定画什么、画多少行、每行长什么样的是适配器。你可以把 ListView 想成一个展架适配器就是往展架上摆货的人数据则是仓库里的货。展架不会自己去仓库拿货它只会问适配器一共有多少件第 n 件是什么这件货该用哪个包装盒适配器回答完这三个问题ListView 才能把界面渲染出来。Android 里最基础的适配器是 BaseAdapter它是一个抽象类定义了上面那几个必须回答的问题。ArrayAdapter、SimpleAdapter、SimpleCursorAdapter 都继承自它。ArrayAdapter 是最简单的一种适合“每行只显示一段文字”的列表比如设置项、城市列表、日志行。它的构造方法通常接收三个参数上下文 Context、每一行的布局资源 ID、以及数据源 List。这里的布局资源指的是“单行长什么样”而不是整个页面的布局这一点新手特别容易搞混。系统内置了android.R.layout.simple_list_item_1这种单行布局只显示一行文字拿来练手最合适。这篇教程的目标很明确带你从零跑通第一个 ArrayAdapter 列表把布局文件、Activity 代码、清单注册、跳转入口全部写全并且顺手把示例里可能用到的模型调用配置统一管起来。为什么要提模型调用配置因为现在很多 Android 练手项目会顺带接一个 AI 能力比如把列表项做智能分类、生成摘要。如果每个 Demo 都散落着 Key 和 Base URL后面维护会很痛苦。TaoToken 提供的就是一个统一的 Key/API 通道把模型调用配置集中管理示例代码里只留一个入口换模型、换 Key 都不用翻遍整个工程。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 后面配置章节会给出可直接复制的片段。适配器的工作原理其实不复杂。当 ListView 需要显示时它会调用适配器的getCount()拿到总行数然后对每一行调用getView(int position, View convertView, ViewGroup parent)。getView是核心它负责把第 position 条数据填充到一行视图里并返回。convertView是回收复用的旧视图ListView 只创建屏幕上可见的那几行滑出屏幕的行会被回收滑进来的行复用这些旧视图所以列表再长也不会创建成千上万个 View。理解这一点后面学 ViewHolder 优化就顺理成章了。ArrayAdapter 已经帮你实现了getView你只需要把数据丢进去它自动用simple_list_item_1渲染每一行。这也是它“简单”的原因代价是定制能力弱只能显示一行文字。我试过在同一个工程里放多个适配器示例用按钮跳转这样对比起来很直观。下面就从建工程开始一步步把 ArrayAdapter 的最小可运行版本搭出来。整个过程不需要任何第三方库Android Studio 自带的环境就够。你跟着敲一遍跑出列表之后再回头看适配器那几个方法会有完全不一样的感觉。2. TaoToken 前置把模型调用配置从 Demo 里抽出来在写列表代码之前先把模型调用的配置通道准备好。很多同学写 Demo 时习惯把 API Key 硬编码在 Activity 里或者散落在各个工具类中结果就是换一个 Key 要全局搜索替换提交代码时还容易把 Key 一起提交上去。TaoToken 的思路是提供一个统一的 API 入口你只需要记住一个 Base URL 和一个 Key模型 ID 按需切换。对于 Android 示例项目来说这意味着你可以在一个配置文件里集中管理这些参数列表适配器相关的代码完全不碰这些细节。先明确三个要素这也是后面所有接入场景都要写全的三件套Base URL、API Key、Model ID。Base URL 统一用https://taotoken.net/api注意这个地址不带任何查询参数是纯 API 根路径。API Key 需要你在控制台创建创建入口在 https://taotoken.net/console/api-keys 登录后新建一个 Key复制出来保存好它只显示一次。Model ID 则取决于你要调用的模型比如做文本分类可以用通用的对话模型具体可用的模型列表在文档里查文档入口是 https://taotoken.net/doc 。如果你只是想先验证通道是否通可以用模型对话页面直接试入口是 https://taotoken.net/models 不用写代码就能发一条请求看返回。为什么要在 Android 列表教程里讲这个因为一个很典型的练手场景是ListView 展示一批待处理文本点击某一项时调用模型做摘要或分类然后把结果更新回列表。这时候适配器的数据源会动态变化而模型调用的配置如果写死在 Adapter 里测试和生产环境切换会非常麻烦。把配置抽到一个单独的类或者local.properties里Adapter 只负责展示调用逻辑交给 ViewModel 或工具类职责就清晰了。TaoToken 的 Key 可以按项目创建多个比如一个用于调试、一个用于演示互不干扰。具体到 Android 工程里我建议把 Base URL、Key、Model ID 放在local.properties或者一个单独的AiConfig.kt中不要提交到版本库。如果你用 Gradle 的buildConfigField可以在build.gradle里读取环境变量再注入这样代码里通过BuildConfig访问既安全又方便切换。下面给一个 Kotlin 的配置类示例Java 项目照着改即可。注意这里只是配置的存放真正的网络请求后面章节会讲。配置类里三个字段和上面说的三件套一一对应缺一不可。// AiConfig.kt object AiConfig { // 统一 API 入口不带查询参数 const val BASE_URL https://taotoken.net/api // 从 local.properties 或环境变量读取不要硬编码 const val API_KEY BuildConfig.TAOTOKEN_API_KEY // 按需切换模型 ID const val MODEL_ID your-model-id }如果你用的是 Java写法类似把object换成final class加静态字段即可。关键点是Adapter 和 Activity 里不要出现 Key 字符串所有模型调用都走这个配置类。这样后面你要把示例从调试 Key 换成正式 Key只改一处。另外TaoToken 的 Coding Plan 适合长期做编码和 Agent 类项目入口在 https://taotoken.net/coding-plan 如果你打算把这个 Demo 扩展成一个持续迭代的项目可以了解一下。Claude Code 相关的接入文档在 https://taotoken.net/ClaudeCodeAnthropic 里面也有 Base URL 和 Key 的配置说明思路和这里一致。配置准备好之后先别急着写网络请求。这一篇的重点是列表模型调用只是作为“配置统一管理”的引子。你只要确保三件套有地方放、不散落就行。等列表跑通下一篇再把这个配置接到具体的请求里那时候你会发现因为配置已经抽出来了接入只是加一个调用方法的事。3. 可复制配置ArrayAdapter 最小示例的完整文件清单现在进入正题把 ArrayAdapter 的最小可运行版本完整写出来。整个示例包含四个文件主界面布局activity_main.xml、列表页面布局arrayadapterdemo.xml、列表 ActivityArrayAdapterDemo.java、以及清单文件AndroidManifest.xml的注册。另外主界面需要一个按钮跳转按钮的监听写在MainActivity里。所有代码都可以直接复制改一下包名就能跑。先看主界面布局。它只放一个按钮点击后跳转到列表页面。这里用 ConstraintLayout 或 LinearLayout 都行我用 LinearLayout 保持简单。?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp Button android:idid/btn_array_adapter android:layout_widthmatch_parent android:layout_heightwrap_content android:text打开 ArrayAdapter 示例 / /LinearLayout然后是列表页面的布局arrayadapterdemo.xml。注意这里包含一个 TextView 作为标题和一个 ListView 作为列表主体。ListView 的 ID 要和 Activity 里findViewById的一致我这里用arrayadapterdemolistview。?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical TextView android:layout_widthwrap_content android:layout_heightwrap_content android:padding16dp android:text这是 ArrayAdapter 的一个例子 / ListView android:idid/arrayadapterdemolistview android:layout_widthmatch_parent android:layout_heightwrap_content / /LinearLayout接下来是核心的ArrayAdapterDemo.java。它继承 Activity在onCreate里设置布局、找到 ListView、构造 ArrayAdapter 并设置给 ListView。ArrayAdapter 的构造参数依次是上下文this、单行布局资源android.R.layout.simple_list_item_1、数据源getData()。getData()返回一个 List里面放几条测试数据。这里我特意用simple_list_item_1它是系统内置的单行文字布局不需要你自己写 item 布局适合入门。package com.example.adapterdemo; import android.app.Activity; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; import java.util.ArrayList; import java.util.List; public class ArrayAdapterDemo extends Activity { private ListView lv; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.arrayadapterdemo); lv findViewById(R.id.arrayadapterdemolistview); lv.setAdapter(new ArrayAdapter( this, android.R.layout.simple_list_item_1, getData() )); } private ListString getData() { ListString data new ArrayList(); data.add(测试数据1); data.add(测试数据2); data.add(测试数据3); data.add(测试数据4); return data; } }主界面的MainActivity负责按钮跳转。用 Intent 从 MainActivity 跳到 ArrayAdapterDemo代码很短。package com.example.adapterdemo; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.widget.Button; public class MainActivity extends Activity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn findViewById(R.id.btn_array_adapter); btn.setOnClickListener(v - startActivity(new Intent(MainActivity.this, ArrayAdapterDemo.class)) ); } }最后是清单文件注册。两个 Activity 都要注册MainActivity 作为启动页需要 intent-filterArrayAdapterDemo 普通注册即可。?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.adapterdemo application android:allowBackuptrue android:labelAdapterDemo android:themestyle/Theme.AppCompat.Light activity android:name.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity android:name.ArrayAdapterDemo / /application /manifest到这里四个文件就齐了。如果你用的是 Kotlin把 Java 代码转成 Kotlin 语法即可逻辑完全一样。注意simple_list_item_1是系统资源不需要你在 res/layout 下创建直接引用android.R.layout.simple_list_item_1。有些同学会误写成R.layout.simple_list_item_1那样会编译报错因为那是系统包下的资源。这个坑我在第一次写的时候也踩过记住加android.前缀。配置部分再补充一点如果你打算在这个 Demo 里接入模型调用把上一节的AiConfig放进工程然后在ArrayAdapterDemo里不要直接引用 Key而是通过AiConfig.BASE_URL这种方式访问。这样列表代码和模型配置解耦后面扩展成点击列表项触发模型请求时改动范围很小。TaoToken 的 API Key 创建入口再贴一次方便你直接去建https://taotoken.net/console/api-keys 创建后复制到local.properties里不要写进 Java 文件。4. 验证请求与成功结果跑起来看到列表才算数代码写完接下来是验证。打开 Android Studio点绿色运行按钮选择模拟器或真机。首次运行会编译 Gradle等它跑完。如果一切正常你会先看到主界面那个按钮点击后跳转到列表页面页面顶部有一行“这是 ArrayAdapter 的一个例子”下面列出四条测试数据。看到这四行字说明适配器已经正确工作。如果列表是空的或者只有标题没有列表项别急下一节专门排查。验证的时候可以顺便观察一下 ListView 的滚动行为。四条数据太少看不出复用效果。你可以把getData()里的数据加到 50 条用循环生成然后上下滑动列表。你会发现滑动很流畅因为 ArrayAdapter 内部已经处理了 convertView 复用。如果你想亲眼看到复用可以自定义一个 Adapter 继承 ArrayAdapter在getView里打日志观察 position 和 convertView 是否为 null。这是理解适配器原理最直接的方式比看十遍文字都管用。// 自定义 Adapter 观察复用仅用于调试 public class DebugAdapter extends ArrayAdapterString { public DebugAdapter(Context context, int resource, ListString objects) { super(context, resource, objects); } Override public View getView(int position, View convertView, ViewGroup parent) { Log.d(AdapterDebug, position position , convertView (convertView null ? null : reused)); return super.getView(position, convertView, parent); } }把DebugAdapter替换掉原来的 ArrayAdapter运行后看 Logcat你会看到前几行 convertView 是 null后面开始出现 reused。这就证明了 ListView 的回收机制在起作用。这个观察对后面学 BaseAdapter 自定义适配器非常关键因为自定义时你要自己处理 convertView写错了就会导致列表错乱或者性能下降。如果你在示例里接了模型调用验证方式会多一步点击某个列表项触发一次请求看返回是否正常。这时候 Base URL 用https://taotoken.net/apiKey 用你在控制台创建的那个Model ID 按文档填。请求成功的话Logcat 里会打印返回内容。如果返回 401说明 Key 不对或没带上如果提示连接失败检查网络权限和 Base URL 是否写错。Android 里网络请求需要INTERNET权限别忘了在清单文件里加uses-permission android:nameandroid.permission.INTERNET /。这个权限不加请求会直接抛异常而且报错信息不一定直观。成功跑通列表之后建议你把工程结构整理一下。把ArrayAdapterDemo放在ui包下AiConfig放在config包下数据类放在model包下。这样后面加 SimpleAdapter、SimpleCursorAdapter 示例时工程不会乱。每加一个示例就在主界面加一个按钮跳转过去。这种“一个 Demo 工程装多个示例”的做法比每次新建工程效率高很多也方便对比不同适配器的差异。验证环节还有一个容易忽略的点setContentView必须在findViewById之前调用。如果顺序反了findViewById会返回 null然后setAdapter就抛空指针。这个错误在初学者代码里很常见报错信息是Attempt to invoke virtual method void android.widget.ListView.setAdapter on a null object reference。看到这个报错先检查setContentView的位置。另外ListView 的 ID 拼写要和布局文件里完全一致大小写敏感arrayadapterdemolistview和arrayAdapterDemoListView是两个不同的 ID。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth这一节把跑这个示例时可能遇到的报错集中过一遍。有些是列表本身的有些是模型调用配置的分开说。先列一个对照表方便你快速定位。报错关键词可能原因排查方向401 UnauthorizedAPI Key 缺失或错误检查 Key 是否复制完整、是否带上 Authorization 头local proxy failed本地网络或代理配置问题检查 Base URL 是否为https://taotoken.net/api不要带多余路径reading choices返回结构解析错误确认请求体格式和模型返回字段检查 JSON 解析代码OAuth 相关报错认证方式不匹配确认使用的是 API Key 而非 OAuth 流程ListView 空白适配器未设置或数据为空检查setAdapter是否调用、getData是否返回空列表空指针 setAdapterfindViewById返回 null检查setContentView是否在findViewById之前编译报错 simple_list_item_1资源前缀写错用android.R.layout.simple_list_item_1不是R.layout401 是最常见的。如果你在 Android 里发请求Header 里要带Authorization: Bearer 你的Key。Key 从 https://taotoken.net/console/api-keys 创建复制时注意不要多复制空格。有些同学把 Key 放在 URL 参数里那样不对要放在 Header。另外Key 如果被撤销或过期也会返回 401去控制台确认一下 Key 状态。local proxy failed这个报错通常和网络环境有关。先确认 Base URL 写的是https://taotoken.net/api没有多余的后缀。如果你在模拟器里跑模拟器的网络和宿主机是隔离的但访问公网 API 一般没问题。如果公司网络有限制可能需要换一个网络环境测试。注意这里不涉及任何代理工具的配置只是检查网络连通性。你可以先用浏览器或 curl 访问一下 API 根路径看是否有响应排除网络问题。reading choices一般出现在解析模型返回的时候。不同模型的返回结构可能略有差异有的返回choices数组有的返回content字段。你要根据实际返回调整解析代码。建议先用模型对话页面发一条请求看看原始返回长什么样入口是 https://taotoken.net/models 。看清楚字段名再写解析比盲写强。如果你用的是 Gson 或 Moshi字段名要和 JSON 完全对应大小写敏感。OAuth 相关报错通常是因为认证方式用错了。TaoToken 的 API 接入用的是 API Key不是 OAuth 授权码流程。如果你在代码里走了 OAuth 的跳转逻辑那肯定对不上。确认你的请求头是 Bearer Token 形式而不是client_id/client_secret那套。文档里对认证方式有说明接入前扫一眼能省很多时间文档入口 https://taotoken.net/doc 。列表本身的报错除了上面表格里的还有一个是“数据更新了但列表没刷新”。ArrayAdapter 的数据源如果是同一个 List 引用调用add、remove后需要调用notifyDataSetChanged()否则 ListView 不知道数据变了。如果你重新构造了一个新的 List 并setAdapter那会刷新但性能不如复用。这个点在后面动态更新列表时会用到先记着。排查的时候Logcat 是你的好朋友。把日志级别调到 Debug过滤你的包名能看到很多有用信息。网络请求的报错通常会打印异常堆栈顺着堆栈找行号比猜快得多。如果报错信息里出现NetworkOnMainThreadException说明你在主线程发了网络请求Android 不允许这样要放到子线程或协程里。这个错误在入门阶段很常见看到就知道是线程问题。6. 语义一致 CTA把配置通道用起来列表跑通之后你手里已经有一个能展示数据的 ArrayAdapter 示例。下一步自然是让它动起来比如点击列表项触发一次模型调用把返回结果显示在列表里。这时候前面准备的 TaoToken 配置就派上用场了。你不需要在 Adapter 里写任何 Key只需要在点击回调里调用一个封装好的请求方法方法内部从AiConfig读取 Base URL、Key 和 Model ID。这样列表的展示逻辑和模型调用逻辑彻底分开改哪边都不影响另一边。如果你还没创建 Key现在可以去 https://taotoken.net/console/api-keys 建一个然后回到工程把local.properties里的TAOTOKEN_API_KEY填上。接入文档在 https://taotoken.net/doc 里面有请求示例和参数说明照着改就能用。想先不写代码验证通道直接去 https://taotoken.net/models 发一条消息看返回是否正常。长期做编码类项目的话Coding Plan 的入口在 https://taotoken.net/coding-plan 可以了解一下额度和管理方式。Claude Code 的接入说明在 https://taotoken.net/ClaudeCodeAnthropic 里面也强调了 Base URL 和 Key 的配置方式和 Android 里的思路一致。把三件套再确认一遍Base URL 是https://taotoken.net/apiAPI Key 从控制台创建Model ID 按文档选。这三个值在 Android 工程里集中放在AiConfigAdapter 和 Activity 通过它访问。这样你后面加 SimpleAdapter 示例、加点击请求、加结果回填都不会把配置搞乱。列表适配器负责展示配置通道负责连接模型各司其职工程才能越写越顺。
返回列表