android ListView详解:从Adapter到复用机制的完整实践
1. 从一次滑动卡顿说起ListView 到底是什么、能做什么、适合谁如果你刚开始接触 Android或者已经写过几个页面但列表一滑就掉帧那 ListView 大概率是你绕不开的第一个“性能课”。它是一个以垂直滚动列表形式展示数据的控件能根据数据条数自适应显示内容早期几乎所有的联系人、设置项、消息流都用它实现。虽然现在官方更推荐 RecyclerView但大量存量项目、教学示例、老版本兼容场景里 ListView 依然活跃而且它把“数据怎么变成一行行 View”这件事暴露得非常直白特别适合用来理解 Adapter 模式、ViewHolder 复用和事件分发。我见过太多初学者把数据一股脑塞进getView()里每次滑动都重新inflate布局、重新findViewById结果列表一长就卡成幻灯片。这篇就按“能直接跑起来”的标准把 ListView 的三个核心元素——ListView 本身、Adapter 适配器、数据源——拆开讲清楚再给出 Java 和 Kotlin 两套可复制代码最后用真实报错带你排查滑动卡顿。适合谁适合刚学完 Activity 想练手列表的初学者也适合想回头补一补复用机制原理的进阶开发者。核心检索词先记住Android ListView 的 Adapter 绑定与 ViewHolder 复用机制。理解了这个你再看 RecyclerView 会轻松很多因为后者的思路几乎是从这里演化出去的。2. 前置准备Adapter 三种类型怎么选以及一个顺手的调试入口ListView 的显示离不开三个元素ListView 负责展示Adapter 负责把数据映射成 View数据源则是具体的字符串、图片或对象集合。按适配器类型分常见的有 ArrayAdapter、SimpleAdapter 和 SimpleCursorAdapter。ArrayAdapter 最简单只能展示一行文字SimpleAdapter 扩展性最好可以自定义图片、按钮、复选框SimpleCursorAdapter 则是 SimpleAdapter 和数据库 Cursor 的结合方便把查询结果直接铺到列表上。选型其实有个很实用的判断纯文本列表用 ArrayAdapter图文混排用 SimpleAdapter数据来自 ContentProvider 或数据库查询用 SimpleCursorAdapter。但只要你需要在行内放可点击按钮、或者要精细控制复用就必须继承 BaseAdapter 自己写。在动手写代码前我习惯先确认一下手头的模型调用链路是否正常避免把环境问题和代码问题混在一起排查。如果你也在用大模型辅助生成或润色 Android 代码片段可以先用 TaoToken 模型对话 快速验证一段 Adapter 代码的逻辑确认没问题再贴进工程里。它的接入文档在 TaoToken 接入文档API 入口统一是https://taotoken.net/api不需要额外配置复杂环境。这里要提醒一句模型对话适合验证“这段 getView 写得对不对”但不要让它直接连你的生产数据库或真实设备代码最终还是要落到本地工程里跑一遍。前置准备做到位后面配置和排障才不会手忙脚乱。3. 可复制配置从 ArrayAdapter 到自定义 BaseAdapter 的完整代码先看最简单的 ArrayAdapter。新建一个 Activity直接用系统布局android.R.layout.simple_list_item_1它只显示一行文字public class MyListView extends Activity { private ListView listView; Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); listView new ListView(this); listView.setAdapter(new ArrayAdapterString( this, android.R.layout.simple_list_item_1, getData())); setContentView(listView); } private ListString getData() { ListString data new ArrayList(); data.add(测试数据1); data.add(测试数据2); data.add(测试数据3); data.add(测试数据4); return data; } }ArrayAdapter 的构造需要三个参数Context、行布局文件、数据源 List。注意这里的布局文件描述的是“每一行”的样子不是整个页面。setAdapter()完成最后绑定。接下来是图文混排的 SimpleAdapter。先定义行布局vlist.xml?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:orientationhorizontal android:layout_widthmatch_parent android:layout_heightmatch_parent ImageView android:idid/img android:layout_widthwrap_content android:layout_heightwrap_content android:layout_margin5dp / LinearLayout android:orientationvertical android:layout_widthwrap_content android:layout_heightwrap_content TextView android:idid/title android:layout_widthwrap_content android:layout_heightwrap_content android:textColor#FFFFFFFF android:textSize22sp / TextView android:idid/info android:layout_widthwrap_content android:layout_heightwrap_content android:textColor#FFFFFFFF android:textSize13sp / /LinearLayout /LinearLayout对应代码用ListMapString, Object作为数据源每个 Map 对应一行public class MyListView3 extends ListActivity { Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SimpleAdapter adapter new SimpleAdapter( this, getData(), R.layout.vlist, new String[]{title, info, img}, new int[]{R.id.title, R.id.info, R.id.img}); setListAdapter(adapter); } private ListMapString, Object getData() { ListMapString, Object list new ArrayList(); MapString, Object map new HashMap(); map.put(title, G1); map.put(info, google 1); map.put(img, R.drawable.i1); list.add(map); // 继续 add 更多 map... return list; } }SimpleAdapter 的后两个参数是关键一个 String 数组表示 Map 的键一个 int 数组表示布局里组件的 id两者按顺序一一对应完成映射。但真正要讲复用必须继承 BaseAdapter。行布局vlist2.xml在刚才基础上加一个按钮Button android:idid/view_btn android:layout_widthwrap_content android:layout_heightwrap_content android:textstring/s_view_btn android:focusablefalse android:layout_gravitybottom|right /注意android:focusablefalse不加这行按钮会抢走 ListView 的焦点导致整行点不动。下面是带 ViewHolder 复用的完整 Adapterpublic class MyAdapter extends BaseAdapter { private LayoutInflater mInflater; private ListMapString, Object mData; public MyAdapter(Context context, ListMapString, Object data) { this.mInflater LayoutInflater.from(context); this.mData data; } Override public int getCount() { return mData.size(); } Override public Object getItem(int position) { return mData.get(position); } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { holder new ViewHolder(); convertView mInflater.inflate(R.layout.vlist2, null); holder.img convertView.findViewById(R.id.img); holder.title convertView.findViewById(R.id.title); holder.info convertView.findViewById(R.id.info); holder.viewBtn convertView.findViewById(R.id.view_btn); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } holder.img.setBackgroundResource((Integer) mData.get(position).get(img)); holder.title.setText((String) mData.get(position).get(title)); holder.info.setText((String) mData.get(position).get(info)); holder.viewBtn.setOnClickListener(v - showInfo()); return convertView; } public static class ViewHolder { public ImageView img; public TextView title; public TextView info; public Button viewBtn; } }Kotlin 版本更简洁用ViewHolder加getTag/setTag思路完全一致class MyAdapter( private val context: Context, private val data: ListMapString, Any ) : BaseAdapter() { private val inflater LayoutInflater.from(context) override fun getCount(): Int data.size override fun getItem(position: Int): Any data[position] override fun getItemId(position: Int): Long position.toLong() override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { val view: View val holder: ViewHolder if (convertView null) { view inflater.inflate(R.layout.vlist2, parent, false) holder ViewHolder(view) view.tag holder } else { view convertView holder view.tag as ViewHolder } holder.title.text data[position][title] as String holder.info.text data[position][info] as String holder.img.setBackgroundResource(data[position][img] as Int) holder.viewBtn.setOnClickListener { showInfo() } return view } class ViewHolder(view: View) { val img: ImageView view.findViewById(R.id.img) val title: TextView view.findViewById(R.id.title) val info: TextView view.findViewById(R.id.info) val viewBtn: Button view.findViewById(R.id.view_btn) } }如果你在团队里用 Claude Code 或类似工具做长期编码把这类 Adapter 模板沉淀成可复用片段会省很多事TaoToken Coding Plan 适合这种持续性的编码场景。Key 的获取在 TaoToken API Keys控制台在 TaoToken Console。配置时记住三件套Base URL 填https://taotoken.net/apiKey 用你申请的那串Model ID 按文档里对应模型填三者缺一不可。4. 验证请求跑起来看结果确认复用真的生效代码写完后最直接的验证就是跑一遍。用 ListActivity 承载时onCreate里setListAdapter(adapter)即可用普通 Activity 则先setContentView(R.layout.main)再findViewById拿到 ListView 调setAdapter。点击事件分两种。整行点击重写onListItemClickOverride protected void onListItemClick(ListView l, View v, int position, long id) { Log.v(MyListView4-click, (String) mData.get(position).get(title)); }行内按钮点击则在getView里给按钮单独设监听弹个对话框public void showInfo() { new AlertDialog.Builder(this) .setTitle(我的listview) .setMessage(介绍...) .setPositiveButton(确定, null) .show(); }怎么确认 ViewHolder 复用生效在getView里加一行日志打印convertView null的次数Log.d(ListView, position position , convertView is null? (convertView null));滑动一个 100 条数据的列表你会看到前几屏convertView为 null之后全是复用日志里 null 的次数稳定在屏幕可见行数附近。如果每次滑动都打印 null说明复用没生效多半是getView里每次都inflate了新布局或者setTag没写对。再验证一下getCount()的作用把它临时改成返回 0列表直接不显示返回 1只显示一行。这能帮你确认数据长度确实由getCount()决定系统先调它拿到长度再逐行调getView绘制。如果你想让模型帮你检查这段日志逻辑或生成测试数据可以用 TaoToken 模型对话 贴代码问但最终验证一定要在真机或模拟器上跑日志和帧率不会骗人。5. 常见报错排查从 401 到滑动卡顿对照真实错误逐个解决排障这块我按真实遇到的报错来列你可以对照自己的日志找。报错一401 Unauthorized或local proxy failed。如果你在接入模型辅助编码时遇到这个先检查 Key 是否填对、Base URL 是否是https://taotoken.net/api。local proxy failed通常是本地代理配置和实际网络环境冲突把代理关掉、直连 API 入口再试。这类问题跟 ListView 本身无关但会卡住你查文档的节奏先解决掉。报错二java.lang.NullPointerException在getView里。九成是convertView.getTag()拿到的不是 ViewHolder或者findViewById返回 null。检查setTag(holder)是否在convertView null分支里执行以及布局文件里 id 是否和代码一致。报错三reading choices相关解析失败。这通常出现在你让模型返回结构化数据、结果格式不对时。解决办法是在提示里明确要求返回 JSON并给出字段示例别让它自由发挥。报错四OAuth鉴权失败。如果你用的是需要 OAuth 的编码工具检查 token 是否过期、回调地址是否配置正确。这类工具接入时同样记住三件套Base URL、Key、Model ID缺一个都会鉴权失败。报错五滑动卡顿、掉帧。这是 ListView 最典型的性能问题。排查顺序第一确认getView里没有做耗时操作比如读文件、网络请求、大量字符串拼接第二确认 ViewHolder 复用生效别每次inflate第三图片加载别在主线程解码大图用 Glide 或 Coil 这类库第四行布局层级别太深measure/layout次数会随层级上升。我试过把行布局从五层压到两层滑动帧率肉眼可见地稳了。报错六按钮点击无响应。前面提过给 Button 加android:focusablefalse否则它抢焦点导致整行点不动。如果按钮和整行点击都要响应按钮单独设监听整行用onListItemClick。报错七IllegalStateException: The content of the adapter has changed but ListView did not receive a notification。数据变了但没通知刷新。改完数据源后调adapter.notifyDataSetChanged()并且确保数据修改在主线程。排查时建议开 GPU 渲染模式分析看每一帧的耗时条。如果某帧突然变长基本就是getView里藏了重活。把重活挪到异步或者用缓存卡顿会明显改善。6. 把复用机制吃透再去看 RecyclerView 就顺了ListView 的getView(position, convertView, parent)这三个参数其实把复用的全部秘密都摆在了明面上convertView就是系统回收给你的旧行视图你复用它、更新数据、再返回就完成了一次复用。ViewHolder 只是把findViewById的结果缓存进tag避免重复查找。这套思路在 RecyclerView 里被抽象成了onCreateViewHolder和onBindViewHolder两个方法职责更清晰但底层逻辑一脉相承。所以别觉得 ListView 过时就不值得学。把 Adapter 绑定、ViewHolder 复用、点击事件分发、getCount与getView的调用时机这几件事亲手写一遍、跑一遍、用日志验证一遍你对 Android 列表控件的理解就扎实了。真到用 RecyclerView 时你只需要关注它多出来的 LayoutManager 和 ItemDecoration核心的复用思想你已经会了。最后留个实用习惯每次写完 Adapter先在getView里打一行复用日志滑一遍确认 null 次数正常再删日志。这个动作花不了两分钟但能帮你避开后面几小时的卡顿排查。代码跑起来日志看起来比看十篇教程都管用。

相关新闻

Docker Compose 快速部署 WordPress:compose 配置详解与完整实战流程

Docker Compose 快速部署 WordPress:compose 配置详解与完整实战流程

示例工程 【免费下载链接】awesome-compose Awesome Docker Compose samples 项目地址: https://gitcode.com/gh_mirrors/aw/awesome-compose 点击查看 免费下载 本篇技术指南基于 awesome-compose 仓库的官方文档示例(official-documentation-samples/…

2026/9/30 14:31:25 阅读更多 →
一套能写文档、管权限、做AI问答的私有化知识库:zyplayer-doc

一套能写文档、管权限、做AI问答的私有化知识库:zyplayer-doc

一套能写文档、管权限、做AI问答的私有化知识库:zyplayer-doc对于更关注数据安全、有私有化部署知识库需求的企业,zyplayer-doc可以部署在自己的服务器或内网,让资料始终留在企业自己的环境中。系统同时提供文档管理、在线协作、权限控制、全…

2026/9/30 14:31:25 阅读更多 →
健身选补剂别盲目跟风,蛋白科研实力才是企业硬实力

健身选补剂别盲目跟风,蛋白科研实力才是企业硬实力

随着全民健身热潮持续升温,健身营养产品市场规模不断扩张,蛋白粉、肌酸、运动恢复类补剂层出不穷。不少健身爱好者挑选产品时,很容易陷入选购误区:只盯着包装上的蛋白含量数字,轻信营销宣传,忽略品牌背后的…

2026/9/30 14:31:25 阅读更多 →

最新新闻

EPLAN实战指南:从模板配置、线号生成到部件库管理

EPLAN实战指南:从模板配置、线号生成到部件库管理

干了这么多年电气设计,我发现一个很有意思的现象:同一个办公室,有人用EPLAN画一套柜子要一周,有人只要两天,而且改图、出报表、做BOM都干净利落。差别不在于软件版本高低,也不在于谁手速快,而在…

2026/9/30 15:13:23 阅读更多 →
ClawPanel 技术架构解析:Vanilla JS + Rust Tauri 多引擎零框架设计为何胜出

ClawPanel 技术架构解析:Vanilla JS + Rust Tauri 多引擎零框架设计为何胜出

ClawPanel 技术架构解析:Vanilla JS Rust Tauri 多引擎零框架设计为何胜出 【免费下载链接】clawpanel 🦞 OpenClaw & Hermes Agent 多引擎 AI 管理面板 — 内置 AI 助手(工具调用 图片识别 多模态),一键安装 |…

2026/9/30 15:13:23 阅读更多 →
诚实完成的秘密:mercury-agent v1.2.3 完成契约与防卡死恢复架构深度解析

诚实完成的秘密:mercury-agent v1.2.3 完成契约与防卡死恢复架构深度解析

诚实完成的秘密:mercury-agent v1.2.3 完成契约与防卡死恢复架构深度解析 【免费下载链接】mercury-agent Soul-driven AI agent with permission-hardened tools, token budgets, and multi-channel access. Runs 24/7 from CLI, Telegram or More. 项目地址: ht…

2026/9/30 15:13:23 阅读更多 →
pdoom-video 4K 渲染实战:无头 Chrome + ffmpeg 输出真 3840×2160 视频的完整部署

pdoom-video 4K 渲染实战:无头 Chrome + ffmpeg 输出真 3840×2160 视频的完整部署

pdoom-video 4K 渲染实战:无头 Chrome ffmpeg 输出真 38402160 视频的完整部署 【免费下载链接】pdoom-video Code-rendered music video for "Im Upping My P(doom)" 项目地址: https://gitcode.com/gh_mirrors/pd/pdoom-video pdoom-video 是一…

2026/9/30 15:13:23 阅读更多 →
小U 使用说明  功能说明(PASM Studio 内置 AI 伙伴 · v0.31.11)

小U 使用说明 功能说明(PASM Studio 内置 AI 伙伴 · v0.31.11)

小U 是 PASM Studio 内置的桌面 AI 伙伴:能对话、会思考、记得住你、有情绪, 本地运行保护隐私,还能在专业领域(开店 / 跨境电商 / 食品经营)给出"先核验再开口"的建议。 本文面向首次接触小U 的用户,5 分钟完成安装到第一句对话。 一、小U 是什么 小U 不是&q…

2026/9/30 15:13:23 阅读更多 →
2026年品牌出海GEO服务商盘点解析:海外AI获客选哪家出海GEO优化公司?看广拓时代SEO与GEO的执行配合

2026年品牌出海GEO服务商盘点解析:海外AI获客选哪家出海GEO优化公司?看广拓时代SEO与GEO的执行配合

一份完整的产品介绍,不一定能在AI回答里得到完整呈现。海外买家借助助手了解品牌、对照产品、挑选供应商时,看到的是模型重新组织的信息。出海团队因此需要核查品牌和产品有没有被准确识别、引用依据是否可靠,以及文字是否符合当地语言和行业…

2026/9/30 15:12:23 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/30 13:14:22 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/29 16:41:41 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/30 13:14:49 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/29 3:55:56 阅读更多 →