1. Android Adapter 到底在解决什么问题Adapter 是 Android 列表控件里连接数据源和 UI 的中间层ListView、GridView、Spinner、RecyclerView 都靠它把一条条数据映射成屏幕上的一行行 View。你写列表时遇到的“数据改了界面不动”“滑动卡顿”“按钮点不动”“报 IllegalStateException”八成都能在 Adapter 的配置骨架里找到原因。这篇面向正在本地工程里跑列表渲染的 Android 开发者从 BaseAdapter、ArrayAdapter、SimpleAdapter 一路梳理到 RecyclerView.Adapter给出可直接复制的代码骨架、布局片段和验证动作让你快速跑通列表渲染与数据更新。我按“先能跑、再能改、最后能排错”的顺序组织每个 Adapter 都配一个最小可运行示例并说明它适合谁、不适合谁。调试网络数据源时如果接口鉴权、模型调用这类配置想统一走一个 Key/API 通道可以把 TaoToken 作为配置项接入官网入口见文末 CTA正文重点仍是 Adapter 本身。2. 接入前的环境与 TaoToken 配置项2.1 工程与依赖准备本地工程建议用 Android Studio 新建 Empty Views ActivityminSdk设 21 以上即可覆盖绝大多数设备。RecyclerView 需要显式加依赖ListView 系列是 framework 自带不用额外引。// app/build.gradle dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.recyclerview:recyclerview:1.3.2 implementation com.google.android.material:material:1.11.0 }布局里用androidx.recyclerview.widget.RecyclerView全限定名别写成旧包的android.support.v7.widget.RecyclerView否则会报ClassNotFoundException。2.2 TaoToken 作为网络数据源配置项列表数据常来自网络。把请求基址、Key 这类配置集中管理调试时切换环境最省事。TaoToken 提供统一的 Key/API 通道接入时把 API 地址和 Key 放进local.properties或BuildConfig不要硬编码进 Adapter。# local.properties不要提交到版本库 TAOTOKEN_API_BASEhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的key// app/build.gradle 读取并注入 BuildConfig android { buildFeatures { buildConfig true } defaultConfig { buildConfigField String, API_BASE, \${project.findProperty(TAOTOKEN_API_BASE) ?: }\ } }Key 的创建入口在控制台的 API Keys 页面模型对话调试入口见第 6 节 CTA。这样 Adapter 只负责渲染网络层单独抽出来职责清晰排错也快。3. 从 ArrayAdapter 到 BaseAdapter 的可复制骨架3.1 ArrayAdapter一行文字的最短路径ArrayAdapter 支持泛型只能显示一行文字适合设置页、简单选项列表。它有三个常用构造(Context, int layoutRes, T[] objects)、(Context, int layoutRes, ListT objects)、(Context, int layoutRes, int textViewId, ListT objects)。public class SimpleListActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ListView listView new ListView(this); ListString data new ArrayList(); data.add(测试数据1); data.add(测试数据2); data.add(测试数据3); ArrayAdapterString adapter new ArrayAdapter( this, android.R.layout.simple_list_item_1, data); listView.setAdapter(adapter); setContentView(listView); } }simple_list_item_1是系统内置的单行布局里面只有一个TextViewid 是android.R.id.text1。如果你自定义布局必须保证布局里有一个TextView否则 ArrayAdapter 找不到目标控件会抛IllegalStateException: ArrayAdapter requires the resource ID to be a TextView。3.2 SimpleAdapter图文混排的快速方案SimpleAdapter 扩展性好数据源是ListMapString, Object每个 Map 对应一行键映射到布局里的控件 id。适合“图片 标题 副标题”这类固定结构。!-- res/layout/item_simple.xml -- 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_marginEnd8dp / 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 /LinearLayoutprivate 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 更多行 return list; } 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}); listView.setAdapter(adapter);两个数组必须一一对应from里的键顺序要和to里的控件 id 顺序一致长度也要相等否则映射错位或直接崩。SimpleAdapter 的局限是它内部用SimpleAdapter.ViewBinder处理复杂绑定遇到按钮点击、状态切换就不够用了这时该上 BaseAdapter。3.3 BaseAdapter自定义行的完整骨架BaseAdapter 是抽象类需要实现getCount()、getItem()、getItemId()、getView()四个方法。它灵活度最高配合 ViewHolder 模式能扛住复杂行布局。核心是getView()里的复用逻辑convertView为空才 inflate否则直接取getTag()缓存的 ViewHolder。public class MyAdapter extends BaseAdapter { private final LayoutInflater mInflater; private final ListMapString, Object mData; public MyAdapter(Context context, ListMapString, Object data) { this.mInflater LayoutInflater.from(context); this.mData data; } Override public int getCount() { return mData null ? 0 : mData.size(); } Override public Object getItem(int position) { return mData.get(position); } Override public long getItemId(int position) { return position; } static class ViewHolder { ImageView img; TextView title; TextView info; Button viewBtn; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView mInflater.inflate(R.layout.item_vlist2, parent, false); holder new ViewHolder(); 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(); } MapString, Object item mData.get(position); holder.img.setBackgroundResource((Integer) item.get(img)); holder.title.setText((String) item.get(title)); holder.info.setText((String) item.get(info)); final int pos position; holder.viewBtn.setOnClickListener(v - showInfo(pos)); return convertView; } private void showInfo(int position) { new AlertDialog.Builder(mInflater.getContext()) .setTitle(我的listview) .setMessage(第 position 行) .setPositiveButton(确定, null) .show(); } }inflate的第三个参数传parent且attachToRoot为false这样能拿到正确的LayoutParams避免行宽高异常。getItemId()返回position是常见做法如果配合Cursor或数据库主键应返回真实 id。3.4 RecyclerView.Adapter现代列表的标准写法RecyclerView 把“布局管理”和“复用”拆开了Adapter 只负责创建 ViewHolder 和绑定数据不再有convertView参数。骨架分三块继承RecyclerView.AdapterVH、定义 ViewHolder、实现onCreateViewHolder和onBindViewHolder。public class UserAdapter extends RecyclerView.AdapterUserAdapter.UserVH { public interface OnItemClick { void onClick(int position); } private final ListUser data; private final OnItemClick listener; public UserAdapter(ListUser data, OnItemClick listener) { this.data data; this.listener listener; } static class UserVH extends RecyclerView.ViewHolder { TextView name; TextView desc; UserVH(View itemView) { super(itemView); name itemView.findViewById(R.id.tv_name); desc itemView.findViewById(R.id.tv_desc); } } NonNull Override public UserVH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_user, parent, false); return new UserVH(v); } Override public void onBindViewHolder(NonNull UserVH holder, int position) { User user data.get(position); holder.name.setText(user.getName()); holder.desc.setText(user.getDesc()); holder.itemView.setOnClickListener(v - { if (listener ! null) { listener.onClick(holder.getBindingAdapterPosition()); } }); } Override public int getItemCount() { return data.size(); } }Activity 里配置 LayoutManager 并设置 AdapterRecyclerView rv findViewById(R.id.recycler_view); rv.setLayoutManager(new LinearLayoutManager(this)); UserAdapter adapter new UserAdapter(userList, position - { Toast.makeText(this, 点击 position, Toast.LENGTH_SHORT).show(); }); rv.setAdapter(adapter);数据更新用notifyItemInserted、notifyItemRemoved、notifyItemChanged这类精确方法别动不动notifyDataSetChanged()后者会重绘全部可见项滑动时容易掉帧。4. 验证请求与成功结果4.1 本地跑通列表渲染把上面的 Adapter 接进 Activity运行后应看到列表正常滚动行内容与数据源一致。验证动作分三步第一启动后列表首屏渲染无空白第二快速上下滑动行内容不串位、不重复第三点击行或行内按钮回调的 position 与点击位置一致。4.2 验证数据更新在 Activity 里加一个按钮点击后往数据源插入一条并通知 AdapterbtnAdd.setOnClickListener(v - { userList.add(new User(新用户, 动态插入)); adapter.notifyItemInserted(userList.size() - 1); rv.scrollToPosition(userList.size() - 1); });预期结果是列表末尾新增一行并自动滚到底部。如果界面没变化先检查数据源和 Adapter 持有的是不是同一个 List 引用——传副本进去是新手最常踩的坑。4.3 网络数据源联调如果列表数据来自网络把请求封装成 Repository返回ListUser后交给 Adapter。调试接口时用 TaoToken 的统一通道把BuildConfig.API_BASE和 Key 注入请求头日志里打印状态码和响应体长度即可确认链路通。模型对话类调试入口见第 6 节。5. 本篇常见报错排查5.1 ArrayAdapter requires the resource ID to be a TextView自定义布局里没有TextView或根布局不是TextView。解决布局根节点用TextView或改用带textViewId参数的构造并确保该 id 存在。5.2 NullPointerException 在 getView / onBindViewHolderfindViewById返回 null通常是布局里控件 id 写错或convertView复用时 ViewHolder 没正确setTag。检查setTag和getTag是否成对出现onCreateViewHolder里 inflate 的布局是否和findViewById的 id 同源。5.3 行内按钮点击无响应ListView 里 Button 会抢焦点导致行点击失效。在行布局根节点加android:descendantFocusabilityblocksDescendants或给 Button 设android:focusablefalse。RecyclerView 里则用holder.itemView.setOnClickListener配合getBindingAdapterPosition()注意在onBindViewHolder里不要直接捕获position变量做异步操作位置会因复用而错乱。5.4 notifyDataSetChanged 后列表不刷新数据源是同一个引用才会生效。如果你在 Adapter 构造里new ArrayList(data)做了拷贝外部 add 不会反映到 Adapter。解决要么传引用要么在 Adapter 里提供updateData(ListT)方法内部替换并notifyDataSetChanged()。5.5 RecyclerView 不显示 / 报 No layout manager attached忘记setLayoutManager。RecyclerView 必须显式设置 LayoutManager否则不渲染任何内容日志会提示No layout manager attached; skipping layout。5.6 IndexOutOfBoundsException 在 getItemCount 与数据源不一致getItemCount()返回的 size 和onBindViewHolder里访问的索引不同步。多线程更新数据时先在主线程改完数据源再调 notify 方法别在子线程直接改 List。6. 继续调试与接入列表跑通后下一步通常是接真实接口、做分页和局部刷新。网络层配置想统一管理可以在控制台创建 Key把 API 地址和 Key 注入BuildConfig接入文档里有完整的请求示例和参数说明。需要验证模型返回结构时用模型对话页面直接发一条请求看响应格式比在代码里反复打日志快得多。长期做编码和 Agent 类项目Coding Plan 里可以集中管理调用额度。API Keys 创建https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite模型对话调试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAdapter 这块真正省时间的做法是把 ViewHolder 和点击回调抽成基类新列表直接继承少写一半样板代码。