ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

自定义GridView/ListView数据源:用BaseAdapter把接口数据接进列表

自定义GridView/ListView数据源:用BaseAdapter把接口数据接进列表 1. 从接口 JSON 到 GridView 列表自定义 BaseAdapter 数据源绑定到底卡在哪很多人第一次做 Android 列表都会经历同一个过程静态数据用 ArrayAdapter 绑一下跑得挺顺一旦换成接口返回的 JSON列表就不知道怎么喂进去了。ArrayAdapter 只认单一类型的数组而接口数据往往是对象数组字段有标题、图标、状态、点击行为甚至还有嵌套结构。这时候就得自己写一个 BaseAdapter 子类把 JSON 解析出来的对象列表一条条映射到 item 布局的控件上。这篇要解决的就是这条链路接口拿到 JSON → 解析成数据模型 → 自定义 Adapter 继承 BaseAdapter → getView 里复用 convertView ViewHolder → 调 notifyDataSetChanged 刷新 → 在 GridView/ListView 上跑通渲染。适合已经会写 Activity、能看懂 XML 布局但对 Adapter 内部机制还比较模糊的 Android 初学者也适合想回头把列表复用逻辑理清楚的老手。核心检索词先摆出来GridView、ListView、BaseAdapter、Adapter、数据源。这几个词基本覆盖了列表绑定的全部关键点。GridView 和 ListView 是容器BaseAdapter 是桥梁数据源是内容Adapter 负责把内容翻译成 View。理解了这个分工后面写代码就不会乱。我试过最笨的办法是在 getView 里每次都 inflate 一个新 View结果列表一滑动就卡内存也涨。后来才明白 convertView 的意义它是系统回收的旧 item拿来复用就能避免重复创建。再配合 ViewHolder 把 findViewById 的结果缓存起来滑动才顺。下面按可跟做的步骤把整条链路拆开写。2. 接入前的准备TaoToken 配置与 Android 项目环境在写 Adapter 之前先把两件事准备好一是 Android 项目本身能跑二是如果你打算用大模型辅助生成或调试这段列表代码可以先把 TaoToken 的接入配置弄好。TaoToken 是一个大模型 API 聚合平台官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。它本身不替代 Android Studio也不替代你的编辑器只是在你需要让模型帮你补全 Adapter 模板、解释报错、生成 JSON 解析代码时提供一个统一的调用入口。Android 侧的准备很直接Android Studio 新建一个 Empty Views Activity 项目minSdk 用 21 以上即可语言选 Java。布局文件放在 res/layout 下图片资源放 res/drawable。如果你要模拟接口数据可以在本地写一个返回 JSON 字符串的方法或者用 assets 里放一个 json 文件读出来先不依赖网络库把 Adapter 链路跑通再说。TaoToken 侧的配置重点是把 Base URL、API Key、Model ID 三件套对齐。Base URL 用 https://taotoken.net/api Key 在控制台的 API Keys 页面创建地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Model ID 按你实际要用的模型填比如对话类或代码类模型。如果你用的是 Claude Code 这类编码工具接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有 Base URL 和鉴权头的写法。这里要提醒一句TaoToken 的配置是为了让你在写列表代码时能随时问模型不是列表运行的必要条件。列表能不能渲染取决于你的 Adapter 写对没有、数据源有没有值、notifyDataSetChanged 有没有在正确的线程调用。把这两条线分开排障时才不会互相干扰。环境准备好后先建三个文件数据模型类 ItemBean、Adapter 类 MyGridAdapter、布局文件 gridview_item.xml 和 activity_main.xml。下面一节直接给可复制的配置和代码。3. 可复制配置BaseAdapter 子类模板与 ViewHolder 写法这一节是全文的核心直接给能粘贴进项目的代码。先定义数据模型把接口 JSON 里的字段映射成 Java 字段。假设接口返回的每条数据有 title、iconRes、enabled、actionType 四个字段对应标题、图标资源、是否可用、点击行为类型。public class ItemBean { public String title; public int iconRes; public boolean enabled; public String actionType; public ItemBean(String title, int iconRes, boolean enabled, String actionType) { this.title title; this.iconRes iconRes; this.enabled enabled; this.actionType actionType; } }然后是 Adapter 本体。继承 BaseAdapter必须实现 getCount、getItem、getItemId、getView 四个方法。getCount 返回数据源大小getItem 返回当前位置对象getItemId 一般返回 position。getView 是重点里面做 convertView 复用和 ViewHolder 缓存。public class MyGridAdapter extends BaseAdapter { private Context context; private ListItemBean dataList; private LayoutInflater inflater; public MyGridAdapter(Context context, ListItemBean dataList) { this.context context; this.dataList dataList; this.inflater LayoutInflater.from(context); } Override public int getCount() { return dataList null ? 0 : dataList.size(); } Override public Object getItem(int position) { return dataList null ? null : dataList.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) { convertView inflater.inflate(R.layout.gridview_item, parent, false); holder new ViewHolder(); holder.ibIcon convertView.findViewById(R.id.ib_icon); holder.tvTitle convertView.findViewById(R.id.tv_title); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } ItemBean bean dataList.get(position); holder.tvTitle.setText(bean.title); holder.ibIcon.setImageResource(bean.iconRes); holder.ibIcon.setEnabled(bean.enabled); holder.ibIcon.setAlpha(bean.enabled ? 1.0f : 0.4f); holder.ibIcon.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { Toast.makeText(context, 点击 bean.title, Toast.LENGTH_SHORT).show(); } }); return convertView; } static class ViewHolder { ImageButton ibIcon; TextView tvTitle; } }注意 ViewHolder 用 static 修饰避免持有外部类引用导致内存泄漏。getView 里 inflate 时传 parent 和 false这样布局的 layout 参数才会生效同时不会立刻 attach 到父容器。点击事件绑定在 holder.ibIcon 上每次 getView 都会重新设置因为 convertView 是复用的不重设会串位。布局文件 gridview_item.xml 用一个垂直线性布局上面图标下面文字?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:gravitycenter android:padding8dp ImageButton android:idid/ib_icon android:layout_width60dp android:layout_height60dp android:backgroundandroid:color/transparent android:scaleTypecenterInside / TextView android:idid/tv_title android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginTop4dp android:textSize12sp android:textColor#333333 / /LinearLayoutactivity_main.xml 放一个 GridView列数设为 3?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical GridView android:idid/gv_test android:layout_widthmatch_parent android:layout_heightwrap_content android:numColumns3 android:stretchModecolumnWidth android:gravitycenter android:horizontalSpacing8dp android:verticalSpacing8dp / /LinearLayout如果你用的是 ListView把 GridView 换成 ListView去掉 numColumns 和 stretchModeitem 布局改成横向排列即可Adapter 代码完全不用动。这就是 BaseAdapter 的好处容器换了数据绑定逻辑不变。数据源构造和绑定在 Activity 里public class MainActivity extends AppCompatActivity { private GridView gvTest; private MyGridAdapter adapter; private ListItemBean dataList new ArrayList(); Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); gvTest findViewById(R.id.gv_test); dataList.add(new ItemBean(首页, R.drawable.ic_home, true, home)); dataList.add(new ItemBean(返回, R.drawable.ic_back, true, back)); dataList.add(new ItemBean(前进, R.drawable.ic_forward, false, forward)); dataList.add(new ItemBean(设置, R.drawable.ic_setting, true, setting)); adapter new MyGridAdapter(this, dataList); gvTest.setAdapter(adapter); } }到这里静态数据已经能渲染。下一步是把接口 JSON 解析成 List然后调 notifyDataSetChanged。解析部分可以用 Gson 或手动 JSONObject这里给手动解析的写法避免引入额外依赖private ListItemBean parseJson(String json) { ListItemBean list new ArrayList(); try { JSONArray array new JSONArray(json); for (int i 0; i array.length(); i) { JSONObject obj array.getJSONObject(i); String title obj.optString(title); int iconRes obj.optInt(iconRes); boolean enabled obj.optBoolean(enabled, true); String actionType obj.optString(actionType); list.add(new ItemBean(title, iconRes, enabled, actionType)); } } catch (JSONException e) { e.printStackTrace(); } return list; }解析完成后把新数据替换进 dataList再调 adapter.notifyDataSetChanged()。注意 notifyDataSetChanged 必须在主线程调用如果你在子线程解析完 JSON要用 runOnUiThread 包一层。4. 验证请求与成功结果本地跑通列表渲染的检查步骤代码写完怎么确认真的跑通了按下面几步走每一步都有明确的观察点。第一步编译运行。Android Studio 点 Run选一个模拟器或真机。如果编译报错先看是不是 R.drawable.ic_home 这类资源不存在换成你项目里已有的图片或者用 android.R.drawable.ic_menu_add 这类系统图标临时替代。第二步看列表是否出现。正常情况下GridView 会显示 4 个 item每个 item 上面是图标下面是文字。第三个「前进」因为 enabledfalse图标会变半透明点击没有 Toast。其他三个点击会弹出对应标题的 Toast。第三步验证复用。在 getView 里加一行日志Log.d(AdapterTest, position position , convertView (convertView null ? null : reuse));滑动列表观察 Logcat。你会看到前几个 position 的 convertView 是 null后面开始出现 reuse。这说明 ViewHolder 复用生效了。如果每次都是 null说明 convertView 没被回收通常是 item 布局高度设置有问题或者父容器没有正确测量。第四步验证数据刷新。在 Activity 里加一个按钮点击后往 dataList 里加一条新数据然后调 notifyDataSetChangedfindViewById(R.id.btn_add).setOnClickListener(v - { dataList.add(new ItemBean(新增, R.drawable.ic_add, true, add)); adapter.notifyDataSetChanged(); });点击按钮列表应该立刻多出一项。如果没变化检查 dataList 是不是同一个引用notifyDataSetChanged 是不是在主线程调的。第五步验证接口数据。把 parseJson 的结果赋给 dataList再调 notifyDataSetChanged。如果你用本地 JSON 字符串模拟可以这样String mockJson [{\title\:\接口项1\,\iconRes\: R.drawable.ic_home ,\enabled\:true,\actionType\:\a\}, {\title\:\接口项2\,\iconRes\: R.drawable.ic_back ,\enabled\:false,\actionType\:\b\}]; dataList.clear(); dataList.addAll(parseJson(mockJson)); adapter.notifyDataSetChanged();跑起来后列表应该变成两项第二项不可点击。到这一步从 JSON 到列表的完整链路就验证完了。如果你在写这段代码时想让模型帮你检查 getView 的复用逻辑可以用 TaoToken 的模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 把 Adapter 代码贴进去问「这段 getView 有没有复用问题」。长期做 Android 编码或 Agent 类任务可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 按需选用。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 对照列表代码本身跑通后如果你同时接了模型 API 辅助开发可能会遇到几类报错。这里把真实遇到的报错和排查路径列出来方便对照。401 Unauthorized。这个最常见出现在调用 TaoToken API 时。原因通常是 API Key 没填、填错、或者请求头格式不对。检查你的请求头是不是Authorization: Bearer 你的KeyKey 是不是从 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 复制完整。注意 Key 前后不要有空格也不要把它写进前端代码里。local proxy failed。这个报错一般出现在本地开发工具配置了代理但代理没启动或者 Base URL 写成了本地地址。如果你用的是 Claude Code 或类似工具检查 settings 里的 Base URL 是不是 https://taotoken.net/api 不要写成 localhost。这个报错和 Android 列表无关但会阻断你让模型辅助写代码的链路。reading choices 相关报错。这类报错通常出现在解析模型返回结构时代码期望choices[0].message.content但实际返回结构不同或者返回体为空。排查方法是先把原始响应打印出来确认字段路径。如果你在 Android 里用 OkHttp 调模型接口记得在 onResponse 里先 log 整个 body再解析。OAuth 相关报错。如果你用的是需要 OAuth 授权的编码工具报错可能是 token 过期或 scope 不对。重新走一遍授权流程确认回调地址和客户端配置一致。这类问题在接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里有说明。列表侧的常见错单独列一下。一是NullPointerException在 getView多半是 dataList 为 null 或 item 布局里 findViewById 的 id 写错。二是点击事件串位原因是 convertView 复用时没有重新设置 listener或者 listener 里用了错误的 position。三是 notifyDataSetChanged 不生效检查数据源是不是同一个 List 引用如果你重新 new 了一个 List 赋给 adapter旧引用不会更新。四是 GridView 只显示一行检查 item 布局的 layout_height 是不是 wrap_content以及 GridView 的 layout_height 是不是 wrap_content 或 match_parent。还有一个容易忽略的点在 getView 里做耗时操作比如读本地大图、解析复杂 JSON会导致滑动卡顿。正确做法是在数据源准备阶段就把图片和字段解析好getView 只做绑定。6. 语义一致收尾把列表绑定链路固化成自己的模板整篇走下来链路其实很清晰接口 JSON → 解析成 List → 自定义 BaseAdapter → getView 复用 convertView ViewHolder → setAdapter 绑定 → notifyDataSetChanged 刷新。GridView 和 ListView 的差别只在容器配置Adapter 层完全通用。建议你把 MyGridAdapter 和 ViewHolder 这段代码存成一个模板文件下次新项目直接改字段名和布局 id 就能用。重点记住三件事convertView 为 null 时才 inflateViewHolder 用 setTag/getTag 缓存notifyDataSetChanged 在主线程调。这三条守住列表基本不会出大问题。如果你在解析接口数据时字段经常变可以把 ItemBean 换成 MapString, Object但那样会失去类型安全getView 里要做更多类型判断。折中方案是用泛型 Adapter或者引入 Gson 做自动映射。等你把基础版跑顺了再往这个方向优化。最后留一个实用技巧在 getView 里给 item 根布局设置setOnClickListener而不是只给图标设置这样点击区域更大用户体验更好。同时记得在 listener 里用getAdapterPosition()而不是直接捕获 position 变量避免复用导致的错位。这两点做完你的列表就算真正可用了。
返回列表