Android Adapter 用法总结:从 BaseAdapter 到 RecyclerView 的适配器演进与实战

发布时间:2026/10/11 1:20:36
Android Adapter 用法总结:从 BaseAdapter 到 RecyclerView 的适配器演进与实战 1. 从 ListView 到 RecyclerViewAdapter 到底在解决什么问题Adapter 这个词直译是“适配器”放在 Android 列表开发里它干的事情非常具体把一堆数据List、数组、Cursor翻译成一个个能显示在屏幕上的 View。你可以把它想成餐厅里的传菜员——后厨数据源做好菜传菜员Adapter按桌号position把菜端到对应的客人item view面前。ListView、GridView、RecyclerView 这些控件本身不关心数据长什么样它们只负责“摆盘子”真正决定每盘菜里装什么的是 Adapter。如果你是从 ListView 时代过来的开发者大概率写过BaseAdapter的四个方法getCount()、getItem()、getItemId()、getView()。这套接口从 Android 1.0 一直活到今天只是承载它的控件从 ListView 换成了 RecyclerView。RecyclerView 把“布局”和“复用”这两件事拆得更干净LayoutManager 管排列方式ItemAnimator 管动画Adapter 只管绑定数据。所以你会看到RecyclerView.Adapter的方法名变了——onCreateViewHolder负责创建onBindViewHolder负责绑定getItemCount替代了getCount。这篇文章面向的是正在做列表页面、或者准备把老项目里的 ListView 迁移到 RecyclerView 的开发者。我会把 ArrayAdapter、SimpleAdapter、BaseAdapter、RecyclerView.Adapter 这四种最常用的适配器各给一份可直接复制的代码然后重点讲清楚 ViewHolder 复用机制、notifyDataSetChanged的触发时机以及迁移过程中最容易踩的坑。选型建议也会在每一节里给出你照着场景对号入座就行。需要说明的是本文的示例代码基于 AndroidX 和 Kotlin/Java 混排RecyclerView 依赖androidx.recyclerview:recyclerview:1.3.2。如果你还在用 support 库把包名替换一下即可API 层面没有本质区别。2. 四种 Adapter 的完整代码与选型对照2.1 ArrayAdapter一行文字的极简场景ArrayAdapter 是最省事的适配器适合“每行只显示一个字符串”的列表比如设置页的选项、城市列表、简单的日志输出。它支持泛型构造时传入上下文、item 布局、数据集合即可。public class SimpleListActivity extends ListActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] cities {北京, 上海, 广州, 深圳, 杭州}; ArrayAdapterString adapter new ArrayAdapter( this, android.R.layout.simple_list_item_1, cities ); setListAdapter(adapter); } }如果你不想继承 ListActivity也可以手动 setAdapterListView listView new ListView(this); ListString data new ArrayList(); data.add(测试数据1); data.add(测试数据2); listView.setAdapter(new ArrayAdapter( this, android.R.layout.simple_list_item_1, data )); setContentView(listView);simple_list_item_1是系统内置布局只包含一个 TextView。如果你需要稍微定制一下文字颜色或大小可以换成simple_list_item_2两个 TextView或者自己写一个只含 TextView 的布局文件。ArrayAdapter 的局限也很明显它只能绑定一个 TextView无法处理图片、按钮等复杂 item。一旦你的列表项超过一个控件就该换 SimpleAdapter 或 BaseAdapter 了。2.2 SimpleAdapter图文混排的快速方案SimpleAdapter 的数据源是ListMapString, Object每个 Map 对应一行Map 的 key 和布局文件里控件的 id 通过两个数组建立映射。它的扩展性比 ArrayAdapter 好很多可以放 ImageView、TextView甚至 CheckBox。先定义一个 item 布局item_simple.xml?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:orientationhorizontal android:layout_widthmatch_parent android:layout_heightwrap_content android:padding8dp ImageView android:idid/img android:layout_width48dp android:layout_height48dp android:layout_margin5dp / LinearLayout android:orientationvertical android:layout_width0dp android:layout_weight1 android:layout_heightwrap_content TextView android:idid/title android:layout_widthwrap_content android:layout_heightwrap_content android:textSize18sp android:textColor#FF333333 / TextView android:idid/info android:layout_widthwrap_content android:layout_heightwrap_content android:textSize13sp android:textColor#FF888888 / /LinearLayout /LinearLayout然后构造 SimpleAdapterpublic class SimpleAdapterActivity extends ListActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SimpleAdapter adapter new SimpleAdapter( this, getData(), R.layout.item_simple, 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); map new HashMap(); map.put(title, G2); map.put(info, google 2); map.put(img, R.drawable.i2); list.add(map); return list; } }注意from数组和to数组的顺序必须一一对应title映射到R.id.titleimg映射到R.id.img。SimpleAdapter 内部已经帮你做了 ViewHolder 复用你不需要自己写getView。但它的短板在于item 里的控件无法单独响应点击事件比如 item 里放一个 Button你没法给这个 Button 单独设监听器而且数据源用 Map 组织类型不安全容易写错 key。2.3 BaseAdapter完全自定义的经典写法当你需要在 item 里放按钮、CheckBox或者需要根据 position 动态改变 item 样式时BaseAdapter 是 ListView 时代的终极方案。它要求你手动实现 ViewHolder 模式否则滑动时会频繁 inflate 布局导致卡顿。先写 item 布局item_base.xml包含图片、两行文字和一个按钮?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:orientationhorizontal android:layout_widthmatch_parent android:layout_heightwrap_content android:padding8dp ImageView android:idid/img android:layout_width48dp android:layout_height48dp android:layout_margin5dp / LinearLayout android:orientationvertical android:layout_width0dp android:layout_weight1 android:layout_heightwrap_content TextView android:idid/title android:layout_widthwrap_content android:layout_heightwrap_content android:textSize18sp / TextView android:idid/info android:layout_widthwrap_content android:layout_heightwrap_content android:textSize13sp / /LinearLayout Button android:idid/view_btn android:layout_widthwrap_content android:layout_heightwrap_content android:text查看 android:focusablefalse / /LinearLayout注意 Button 上的android:focusablefalse这是 ListView 里 item 点击失效的经典原因——Button 抢走了焦点导致onListItemClick不触发。自定义 Adapter 的完整实现public class MyListViewActivity extends ListActivity { private ListMapString, Object mData; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mData getData(); setListAdapter(new MyAdapter(this)); } 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); map new HashMap(); map.put(title, G2); map.put(info, google 2); map.put(img, R.drawable.i2); list.add(map); return list; } Override protected void onListItemClick(ListView l, View v, int position, long id) { Log.v(MyListView-click, (String) mData.get(position).get(title)); } public void showInfo() { new AlertDialog.Builder(this) .setTitle(我的listview) .setMessage(介绍...) .setPositiveButton(确定, null) .show(); } public final class ViewHolder { public ImageView img; public TextView title; public TextView info; public Button viewBtn; } public class MyAdapter extends BaseAdapter { private LayoutInflater mInflater; public MyAdapter(Context context) { this.mInflater LayoutInflater.from(context); } 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.item_base, parent, false); 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; } } }这里有几个关键点。第一inflate的第三个参数传parent而不是null这样布局的layout_width/layout_height才会生效否则可能出现 item 高度异常。第二ViewHolder 缓存在convertView的 tag 里避免每次findViewById。第三Button 的点击监听器在getView里设置由于 convertView 复用每次绑定都会重新设置监听器这是正确的做法。2.4 RecyclerView.Adapter现代列表的标准写法RecyclerView 把 ViewHolder 的概念提升到了框架层面你不再需要手动写if (convertView null)而是实现onCreateViewHolder和onBindViewHolder两个方法。下面是一个完整的 Kotlin 版本数据类用data class定义。data class User(val name: String, val desc: String, val avatarRes: Int) class UserAdapter(private val data: ListUser) : RecyclerView.AdapterUserAdapter.UserViewHolder() { class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val img: ImageView itemView.findViewById(R.id.img) val title: TextView itemView.findViewById(R.id.title) val info: TextView itemView.findViewById(R.id.info) val btn: Button itemView.findViewById(R.id.view_btn) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_base, parent, false) return UserViewHolder(view) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val user data[position] holder.img.setImageResource(user.avatarRes) holder.title.text user.name holder.info.text user.desc holder.btn.setOnClickListener { Toast.makeText(holder.itemView.context, user.name, Toast.LENGTH_SHORT).show() } } override fun getItemCount(): Int data.size }在 Activity 里绑定class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val recyclerView findViewByIdRecyclerView(R.id.recycler_view) recyclerView.layoutManager LinearLayoutManager(this) recyclerView.adapter UserAdapter(listOf( User(G1, google 1, R.drawable.i1), User(G2, google 2, R.drawable.i2), User(G3, google 3, R.drawable.i3) )) } }RecyclerView 的 Adapter 不需要你手动处理复用onBindViewHolder里的 holder 一定是复用的。但这也带来一个陷阱如果你在onBindViewHolder里设置了某个控件的状态比如 CheckBox 的选中状态必须每次绑定都显式设置否则复用时会出现“串行”现象。这是从 ListView 迁移过来最容易忽略的一点。2.5 四种 Adapter 选型对照表适配器数据源适用场景复用机制迁移难度ArrayAdapter数组/List单行文字列表内部实现低直接换 RecyclerView 简单 AdapterSimpleAdapterListMap图文混排、无交互内部实现中需改为数据类 RecyclerViewBaseAdapter任意复杂 item、有按钮手动 ViewHolder中高方法名需映射RecyclerView.Adapter任意现代列表标准框架强制目标形态选型建议很直接新项目一律用 RecyclerView.Adapter老项目里如果只是简单文字列表可以暂时保留 ArrayAdapter但迁移时优先处理有交互的 BaseAdapter 页面。3. 可复制的配置片段与依赖声明这一节给出迁移和接入时需要的配置文件片段你可以直接复制到项目里。首先是build.gradle模块级的依赖dependencies { implementation androidx.recyclerview:recyclerview:1.3.2 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 }如果你用的是 Kotlin DSLbuild.gradle.ktsdependencies { implementation(androidx.recyclerview:recyclerview:1.3.2) implementation(androidx.appcompat:appcompat:1.6.1) implementation(com.google.android.material:material:1.11.0) }RecyclerView 的布局文件activity_main.xml?xml version1.0 encodingutf-8? androidx.recyclerview.widget.RecyclerView xmlns:androidhttp://schemas.android.com/apk/res/android android:idid/recycler_view android:layout_widthmatch_parent android:layout_heightmatch_parent android:clipToPaddingfalse android:padding8dp /如果你需要分割线可以在代码里添加recyclerView.addItemDecoration( DividerItemDecoration(this, DividerItemDecoration.VERTICAL) )对于从 ListView 迁移的场景AndroidManifest.xml里如果之前用了SimpleCursorAdapter读取联系人记得保留权限声明uses-permission android:nameandroid.permission.READ_CONTACTS /不过现在更推荐用 Room 或 ContentResolver 配合 LiveData把 Cursor 转成数据类再交给 RecyclerView.Adapter这样类型更安全也更容易测试。如果你在团队里需要统一管理多个项目的模型接入配置可以把 Base URL、API Key、Model ID 这三件套写进一个settings.json或config.toml里避免每个开发者手动填。比如{ base_url: https://taotoken.net/api, api_key: sk-你的密钥, model_id: claude-sonnet-4-5 }这个配置在接入文档里有更详细的字段说明你可以对照自己的项目结构调整。需要生成密钥的话在 API Keys 页面创建即可。4. 验证请求与成功结果怎么确认 Adapter 真的在工作写完 Adapter 只是第一步你得验证它确实按预期绑定了数据、复用了 ViewHolder、在数据变化时正确刷新。下面给出几个可操作的验证步骤。第一步验证getItemCount和getCount的返回值。在 Adapter 里加一行日志override fun getItemCount(): Int { Log.d(AdapterDebug, itemCount ${data.size}) return data.size }运行后看 Logcat如果列表有 3 条数据但日志打印 0说明数据源在设置 Adapter 之后才填充需要调整顺序或调用notifyDataSetChanged()。第二步验证 ViewHolder 复用。在onBindViewHolder里打印 position 和 holder 的 hashCodeoverride fun onBindViewHolder(holder: UserViewHolder, position: Int) { Log.d(AdapterDebug, bind position$position holder${holder.hashCode()}) // ... 绑定逻辑 }滑动列表你会看到 position 递增但 holder 的 hashCode 会在几个值之间循环——这就是复用生效的证据。如果每次 hashCode 都不同说明onCreateViewHolder被反复调用可能是layout_height设成了wrap_content且没有固定高度或者 item 布局过于复杂导致 RecyclerView 无法复用。第三步验证notifyDataSetChanged的触发时机。对于 ListView 的 BaseAdapter调用notifyDataSetChanged()会重绘所有可见 item对于 RecyclerView更推荐用notifyItemInserted、notifyItemRemoved、notifyItemChanged这些细粒度方法。你可以这样测试fun addUser(user: User) { data.add(user) notifyItemInserted(data.size - 1) }如果错误地调用了notifyDataSetChanged()日志里会看到所有 position 重新绑定而用notifyItemInserted只会绑定新插入的那一项。这个差异在长列表里对性能影响很大。第四步验证点击事件。在onBindViewHolder里给 itemView 设置点击监听holder.itemView.setOnClickListener { Toast.makeText(it.context, 点击了 ${data[position].name}, Toast.LENGTH_SHORT).show() }注意这里用的是holder.getAdapterPosition()还是闭包里的position在 RecyclerView 1.3.2 之后推荐用holder.bindingAdapterPosition避免位置错乱。成功的结果应该是列表正常显示数据滑动流畅无卡顿点击 item 弹出对应位置的提示插入或删除数据后列表局部刷新而不是整体闪烁。如果你在验证模型对话或 coding-plan 相关的接口返回时也可以用类似的日志方式确认请求参数和响应结构。5. 常见报错排查从 local proxy failed 到 reading choices这一节整理迁移和接入过程中最常遇到的几类报错每个都给出原因和解决路径。报错一local proxy failed或连接超时。这类错误通常出现在你通过 HTTP 客户端请求模型接口时。先检查 Base URL 是否写成了https://taotoken.net/api注意末尾不要多加斜杠。然后确认网络权限已经在AndroidManifest.xml里声明uses-permission android:nameandroid.permission.INTERNET /如果是模拟器确认宿主机网络正常如果是真机确认没有开启飞行模式。这个报错和 Adapter 本身无关但很多开发者在列表里加载远程数据时会遇到所以放在这里一并说明。报错二401 Unauthorized。说明 API Key 无效或未携带。检查请求头里是否有Authorization: Bearer sk-xxx以及 Key 是否复制完整不要有多余空格。如果你用的是 Codex 的auth.json配置确认字段名是api_key而不是apikey。在 API Keys 页面可以重新生成密钥旧密钥会立即失效。报错三reading choices或choices is null。这是解析模型响应时常见的空指针。原因通常是响应体结构和预期不一致比如你按 OpenAI 格式解析choices[0].message.content但实际返回的是流式 chunk。解决办法是先打印原始响应体Log.d(ApiDebug, raw response $responseBody)确认结构后再写解析逻辑。如果是流式响应需要按data:行逐行解析遇到[DONE]结束。报错四OAuth相关错误。如果你在接入 Claude Code 或类似工具时看到 OAuth 报错通常是因为 token 过期或回调地址不匹配。检查配置文件里的redirect_uri是否和平台登记的一致以及系统时间是否准确时间偏差超过几分钟会导致签名校验失败。报错五notifyDataSetChanged不生效。这是 Adapter 层面的经典问题。原因可能有三个数据源是同一个引用且没有真正改变调用notifyDataSetChanged时不在主线程或者 RecyclerView 的 LayoutManager 没有设置。排查顺序是先确认数据确实变了再确认在主线程调用最后检查recyclerView.layoutManager是否为 null。报错六item 点击无响应。如果 item 里有 Button 或 CheckBox它们会默认获取焦点导致 itemView 的点击事件被拦截。解决办法是在这些子控件上设置android:focusablefalse和android:clickablefalse或者直接给子控件设置自己的点击监听器。报错七图片错位。在 RecyclerView 里异步加载图片时如果快速滑动可能出现图片显示到错误位置。这是因为 ViewHolder 被复用时之前的异步请求返回后仍然设置了图片。解决办法是在onBindViewHolder里给 ImageView 设置一个 tag比如图片 URL异步回调时比对 tag 是否一致不一致就丢弃。这些报错里401 和 local proxy failed 属于接入配置问题reading choices 和 OAuth 属于响应解析问题notifyDataSetChanged 和点击无响应属于 Adapter 使用问题。分清楚类别排查起来会快很多。6. 迁移落地与长期维护建议把 ListView 迁移到 RecyclerView本质上不是“换个控件”而是把列表的职责重新划分。ListView 时代Adapter 既要管数据绑定又要管复用还要管点击RecyclerView 把这些拆成了 Adapter、LayoutManager、ItemDecoration、ItemAnimator 四个角色。迁移时建议按这个顺序推进先把数据模型从MapString, Object换成data class再把getView的逻辑拆到onCreateViewHolder和onBindViewHolder最后把notifyDataSetChanged替换成细粒度的 notify 方法。对于还在维护的老项目不必一次性全部迁移。可以先把新页面用 RecyclerView 写老页面保持不动等有重构需求时再逐个替换。替换时注意保留原来的点击回调接口避免上层业务代码大改。如果你在团队里需要统一管理多个项目的模型接入配置可以把 Base URL、API Key、Model ID 写进一个共享的settings.json配合版本控制做环境隔离。需要生成新的密钥时在 API Keys 页面操作即可接入细节可以参考接入文档如果想先验证模型返回结构再写解析逻辑可以用模型对话快速试一次请求。长期做编码和 Agent 类项目的话Coding Plan 会更适合省去每次手动配置的麻烦。最后给一个实用技巧在 RecyclerView 的 Adapter 里加一个DiffUtil可以让列表更新从“整体刷新”变成“精准刷新”配合ListAdapter使用效果更好。这是从 ListView 迁移过来后最值得升级的一个点尤其适合数据频繁变化的聊天列表或动态流。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询