ARTICLE DETAIL

资讯详情

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

Java+Android水果健康百科App:数据驱动、搜索筛选与健康指数实现

Java+Android水果健康百科App:数据驱动、搜索筛选与健康指数实现 简介一份面向高校毕业设计的Android水果健康百科App完整工程源码包以Java为主要开发语言覆盖需求分析、界面设计、编码、测试与部署全流程适合Android初学者、应届毕业生模仿学习或二次拓展同时也便于理解完整移动端项目从零搭建的组织方式。资源共421个文件约18.64MB包括205张png图片素材、142个xml布局与配置文件、40个java核心逻辑源码以及gradle构建脚本、jar依赖、项目说明等目录结构清晰可直接导入Android Studio编译运行。项目内容涉及Activity、Service等核心组件常用布局、SQLite数据存储、网络请求与JSON解析、RecyclerView列表适配、Glide图片加载、动态权限管理等关键开发点并实现水果营养信息展示、健康饮食指导、动态发布与个人中心等具体页面模块各模块间层次分明。已有284人学习下载适合作为毕业设计或课程设计参考也可用于在实际项目中系统提升Android开发能力。1. 把 Java Android 的水果健康百科做成数据驱动的内容型 App而不是静态画册水果健康百科这个题目翻车率最高的做法是做成静态画册一个水果配一张图、一段简介滑动到底就结束了评审看不到数据从哪来、搜索怎么匹配、健康结论怎么算。真正能撑起答辩的版本是把它做成数据驱动的检索型 App。这里给出一套基于 Java Android 的实现路径assets 里放结构化 JSON 做数据源Gson 解析RecyclerView 展示列表Activity 间传递可序列化对象列表之外再补齐搜索、筛选、健康指数与收藏全工程不依赖云服务器演示时关网也能完整跑通。适合课程设计做到中期仍缺少数据流的同学也适合想拿一份能讲的 Java 面试项目做准备的开发者。后端选型、数据结构和边界条件的取舍会直接决定答辩时能不能接住老师的追问。2. 工程骨架与数据层Java 项目结构、JSON 数据源与 Gson 解析2.1 技术选型为什么毕业设计选 Java 而不是 Kotlin这个题目里 Java 是被写进标题的选型上没有太多回旋余地。但答辩仍然会问“为什么不用 Kotlin”常见答法是把 Java 定位成团队协作成本最低的选项代码要能对照教材和网课同一个组里每个人都能看懂集合、继承、接口这套东西而 Kotlin 的协程、空安全、扩展函数虽然本身不复杂却会在演示现场把讨论重点引向语法而不是功能逻辑。在纯功能实现上Java 的劣势也不明显。一个百科类 App 不涉及高频并发不需要协程来简化回调Room、Gson、RecyclerView 全部有成熟的 Java 接口。把构造函数、getter/setter、匿名内部类写熟反而能在老师追问“Java 基础”的时候快速举出实际例子。对比项Java 方案Kotlin 方案课程衔接教材和主流网课仍是 Java代码能对照讲义新教材在切换但老案例数量少工程模板Android Studio 新建项目时可手动选 Java默认模板代码量确实更少答辩追问常问集合、线程、生命周期常追问协程、空安全、扩展函数面试准备Java 基础、JVM、并发部分直接复用还需要补一套 Kotlin 语法题2.2 工程结构Android Studio 新建项目与 Gradle 配置在 Android Studio 里新建 Empty Views ActivityLanguage 选 JavaMinimum SDK 选 API 21。这个起步配置能覆盖 Android 5.0 以上的绝大多数演示机同时避免把时间花在运行时权限和分区存储适配的细枝末节上。包结构建议按 model、repository、adapter、activity、util 拆数据相关代码收拢到 repository 里后续换数据源时只改一个类。build.gradle 里需要引入的东西比想象中少核心是 RecyclerView、Material 组件和 Gson。plugins { id com.android.application } android { compileSdk 34 defaultConfig { applicationId com.example.fruitpedia minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.recyclerview:recyclerview:1.3.2 implementation com.google.android.material:material:1.11.0 implementation com.google.code.gson:gson:2.10.1 }这里的版本号以你本机 Android Studio 模板实际拉到的版本为准不必机械照抄。三个 SDK 参数的分工是compileSdk 决定你能调用哪个版本的 Android 官方 APIminSdk 是安装门槛targetSdk 决定系统按哪个版本的行为规则约束这个应用。参数本项目取值作用compileSdk34决定可用的 Android API 级别minSdk21低于 Android 5.0 的设备无法安装targetSdk34系统按该版本的行为规则运行应用注意新建工程时如果项目文件是 .kts 结尾那是 Kotlin DSL 写法不是选错语言。Java 代码和 Gradle DSL 没有绑定关系不用重开项目。2.3 Fruit 实体与 assets JSON字段设计决定搜索和筛选能不能做很多水果百科项目死在第一步把“水果”简单建模成名字和图片。等到要做搜索、热量筛选、应季推荐时发现字段不够又回去改表。这里借鉴内容型 App 的常见做法在一开始就把每条水果记录设计成一组结构化属性。字段类型示例用途idint101主键后续收藏靠它nameString苹果中文名搜索主字段latinNameStringMalus domestica学名英文搜索也走它calorieint52每 100g 热量筛选要用sugarfloat10.3每 100g 糖分健康指数要用fiberfloat2.4膳食纤维健康指数要用seasonString9,10,11应季月份逗号分隔benefitString富含果胶和维生素C功效简介tabooString胃酸过多者不宜空腹食用禁忌人群imageStringapple本地 drawable 名或 URL实体类本身不涉及 Android 专属 API就是一个带序列化能力的 Java Bean。实现 Serializable 是为了能通过 Intent 在 Activity 之间传递整个对象。public class Fruit implements Serializable { private int id; private String name; private String latinName; private int calorie; // 每 100g 热量单位 kcal private float sugar; // 每 100g 糖分单位 g private float fiber; // 膳食纤维 private String season; // 应季月份如 9,10,11 private String benefit; private String taboo; private String image; public Fruit() { } // Gson 反序列化需要无参构造 // getter / setter 由 IDE 自动生成 }对应的 JSON 放在app/src/main/assets/fruits.json键名和 Java 字段保持一致Gson 才能直接映射。数据量在几百条以内时这是最稳的本地数据源方案。[ { id: 1, name: 苹果, latinName: Malus domestica, calorie: 52, sugar: 10.3, fiber: 2.4, season: 9,10,11, benefit: 富含果胶和维生素C有助于控制餐后血糖波动, taboo: 胃酸过多者不宜空腹食用, image: apple } ]2.4 数据读取与 Gson 解析本地数据源的加载与容错数据放 assets 里不意味着可以直接 new 一个 File 去读必须通过 Context.getAssets() 拿到输入流。流式读取比一次 read 到固定数组更稳妥因为 InputStream.read(byte[]) 不保证一次读满按块循环读是更通用的写法。public class FruitRepository { public static ListFruit loadFromAssets(Context context) { String json readAssets(context, fruits.json); if (json null || json.isEmpty()) { return Collections.emptyList(); } return new Gson().fromJson(json, new TypeTokenListFruit() {}.getType()); } private static String readAssets(Context context, String fileName) { try (InputStream in context.getAssets().open(fileName); ByteArrayOutputStream out new ByteArrayOutputStream()) { byte[] buffer new byte[1024]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } return out.toString(UTF-8); } catch (IOException e) { Log.e(FruitRepository, read asset failed: fileName, e); return null; } } }Gson 的 fromJson 需要知道目标泛型类型直接传 List.class 拿到的会是 List edtreemap 运行期再转 Fruit 会抛 ClassCastException。这里用 TypeToken 就是为了避开 Java 泛型擦除让 Gson 知道要把 JSON 数组反序列化成 List 。注意assets 里的数据打进 APK 后是只读的。如果题目要求“后台可以更新水果数据”就要换成 Room 或服务端接口assets 方案只适合毕业设计这个规模。数据方案适合场景这个项目的取舍assets JSON只读百科数据演示稳定首选项离线可跑Room / SQLite收藏历史、模糊查询、增删改单表百条数据没必要引入后端 API多人协作、实时更新需要服务器答辩风险高3. Android 列表与详情页RecyclerView 复用、点击事件与对象传递3.1 RecyclerView 列表页ViewHolder 复用与 item 布局列表页在 Activity 里只需一个 RecyclerViewitem 布局保持简单左侧水果缩略图右侧名称和热量底部一条分割线。复杂布局要等数据模型稳定后再加一开始就上 CardView 嵌套会显著影响列表滑动帧率。Adapter 的核心是 ViewHolder 复用。onCreateViewHolder 只负责创建视图onBindViewHolder 只做数据绑定不要在 onBind 里 findViewById也不要做耗时计算。public class FruitAdapter extends RecyclerView.AdapterFruitAdapter.ViewHolder { private final ListFruit data new ArrayList(); private OnItemClickListener listener; NonNull Override public ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_fruit, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { Fruit fruit data.get(position); holder.name.setText(fruit.getName()); holder.calorie.setText(String.format(Locale.CHINA, %d kcal/100g, fruit.getCalorie())); holder.itemView.setOnClickListener(v - { if (listener ! null) { listener.onClick(fruit); } }); } Override public int getItemCount() { return data.size(); } public void setData(ListFruit list) { data.clear(); data.addAll(list); notifyDataSetChanged(); } public void setOnItemClickListener(OnItemClickListener listener) { this.listener listener; } interface OnItemClickListener { void onClick(Fruit fruit); } static class ViewHolder extends RecyclerView.ViewHolder { TextView name; TextView calorie; ViewHolder(View itemView) { super(itemView); name itemView.findViewById(R.id.tv_fruit_name); calorie itemView.findViewById(R.id.tv_calorie); } } }点击回调里传递的是 Fruit 对象而不是 position这是个容易被忽略的细节。列表数据经过搜索、筛选后位置会变化如果在点击时用 position 去原数据源取对象取到的可能已经不是用户看到的那一条。回调持有对象本身位置变化不影响正确性。setData 里用了 notifyDataSetChanged是偷懒但正确的写法。百条数据量级刷新一次不过几毫秒答辩现场不会卡如果列表涨到千条以上再考虑 DiffUtil 按 id 计算增量。3.2 点击跳转详情页Intent 携带 Serializable 对象的边界列表页拿到点击的 Fruit 后最常见的跳转方式是把它放进 Intent。因为 Fruit 实现了 SerializableputExtra 和 getSerializableExtra 可以直接工作。Intent intent new Intent(MainActivity.this, FruitDetailActivity.class); intent.putExtra(fruit, fruit); startActivity(intent);Fruit fruit (Fruit) getIntent().getSerializableExtra(fruit);这种写法能跑但不优雅。问题在于 Intent 传递的数据最终要进 Binder 缓冲区它的容量上限只有 1MB 左右。水果列表里如果塞了大的图片对象或者字段越加越多序列化整个对象就有崩溃风险。更稳妥的常见做法是 Intent 只传 id详情页在自己的生命周期里通过 FruitRepository 重新查一次按需加载。这样还能在答辩时补一句“列表到详情的数据流经过了 repository 层而不是把对象强塞进 Intent”。如果希望两者兼顾写一个 getFruitById(int id) 方法数据从同一个全量 List 里查成本极低。public static Fruit findById(ListFruit list, int id) { for (Fruit fruit : list) { if (fruit.getId() id) { return fruit; } } return null; }详情页布局建议用 ScrollView 包裹整块内容而不是 RecyclerView百科详情是固定一屏信息不需要复用机制。顶部图片、中间营养参数表格、底部禁忌说明这种线性结构用 LinearLayout 或 ConstraintLayout 就够了。3.3 图片加载与等待反馈本地资源、Glide 与 Android 进度条的取舍图片处理是这个项目里最容易翻车的一环。如果 image 字段存的是 drawable 资源名比如 apple那可以直接用资源 id 加载但资源名不能用字符串拼接 R.drawable.xxx必须走 getIdentifier。int resId context.getResources() .getIdentifier(imageName, drawable, context.getPackageName());如果 image 字段放的是网络图片 URL则建议引入 Glide。Glide 在图片加载前显示占位图失败时显示错误图这两个机制其实替代了列表场景里大部分 Android 进度条的需求。在列表里放一个无限转圈的 ProgressBar通常意味着图片加载设计有问题。String image fruit.getImage(); if (image null || image.isEmpty()) { holder.ivFruit.setImageResource(R.drawable.ic_fruit_default); } else if (image.startsWith(http)) { Glide.with(holder.itemView.getContext()) .load(image) .placeholder(R.drawable.ic_loading) .error(R.drawable.ic_error) .into(holder.ivFruit); } else { Glide.with(holder.itemView.getContext()) .load(getDrawableResIdByName(image)) .placeholder(R.drawable.ic_loading) .error(R.drawable.ic_error) .into(holder.ivFruit); }这里把本地资源也交给 Glide 加载是为了统一图片管线。后续把 image 字段从 drawable 资源名换成 URLUI 层代码不用动。演示时建议一律用本地资源网络加载不可控现场换网络环境很容易变成加载失败的尴尬局面。4. 水果健康百科的搜索、筛选与健康指数逻辑层该有的三个纵深4.1 关键词实时搜索TextWatcher 的回调时机与过滤逻辑搜索框推荐用 EditText 而不是 SearchView原因是自定义逻辑更直观答辩时也更容易讲清输入监听、数据过滤、列表刷新这条链路。TextWatcher 有三个回调方法实际做过滤要放在 afterTextChanged此时拿到的才是用户输入完成的最终文本。EditText etSearch findViewById(R.id.et_search); etSearch.addTextChangedListener(new TextWatcher() { Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } Override public void onTextChanged(CharSequence s, int start, int before, int count) { } Override public void afterTextChanged(Editable s) { String keyword s.toString().trim().toLowerCase(); ListFruit filtered new ArrayList(); for (Fruit fruit : fruitList) { if (fruit.getName().toLowerCase().contains(keyword) || fruit.getLatinName().toLowerCase().contains(keyword) || fruit.getBenefit().toLowerCase().contains(keyword)) { filtered.add(fruit); } } fruitAdapter.setData(filtered); } });匹配规则解释起来很简单名称、学名、功效三个字段里任意一个包含关键词就命中。中文检索不需要分词自带 contains 在百条数据量级完全够用别为这个场景引入全文搜索库。数据量再大的话用 Handler 的 removeCallbacks 和 postDelayed 组合做 300ms 防抖让用户停止输入后再触发一次搜索能避免每个按键都全表遍历。注意不要在 onTextChanged 里做过滤。这个回调在文本变化过程中会被多次触发拿到的中间文本经常是残的过滤结果会闪跳。统一放 afterTextChanged。4.2 多条件组合筛选热量上限与应季月份一起生效搜索解决的是“找得到”筛选解决的是“收得窄”。百科 App 常见的筛选维度是热量区间、应季月份、含糖量上限。这里用一个独立方法处理输入全量列表和筛选条件输出符合条件的子集。private ListFruit applyFilter(ListFruit source, int maxCalorie, int month) { ListFruit result new ArrayList(); for (Fruit fruit : source) { boolean matchCalorie maxCalorie 0 || fruit.getCalorie() maxCalorie; boolean matchMonth month 0 || containsMonth(fruit.getSeason(), month); if (matchCalorie matchMonth) { result.add(fruit); } } return result; } private boolean containsMonth(String season, int month) { for (String part : season.split(,)) { if (Integer.parseInt(part.trim()) month) { return true; } } return false; }两个条件都设计成“0 表示不限”这样 UI 层不管用户选没选最终都会落成一个不冲突的参数。每次筛选都基于全量数据重算而不是在上一次筛选结果上继续筛避免条件叠加后数据被逐步筛没排查问题也更容易。season 字段在 JSON 里是 9,10,11 这种字符串过滤时用 split 拆成数组逐个匹配。真正处理时建议在 FruitRepository 加载阶段把脏数据清洗掉比如空字符串、乱加的换行进内存之后都是标准格式业务层就不用到处判空。搜索和筛选要同时生效时正确顺序是先按关键词过滤得到中间列表再对这个中间列表调用 applyFilter。先筛选再搜索的问题在于筛选后的列表变化会触发文本变化监听导致搜索再执行一遍产生重复计算。4.3 健康指数一套可解释的简化评分模型水果健康百科如果只有“搜得到、能筛选”功能上还是个词典。健康指数是拉开差距的地方它的本质是把 calorie、sugar、fiber 这些原始营养字段换算成一个用户能直接理解的分数。这里的评分模型不是营养学标准而是可解释的演示模型。private static final int BASE_SCORE 60; public static int calcHealthyScore(Fruit fruit) { double score BASE_SCORE; score (100 - Math.min(fruit.getCalorie(), 100)) / 10.0; // 热量越低加分越多 score - fruit.getSugar() * 2; // 糖分高扣分 score Math.min(fruit.getFiber(), 6) * 2; // 膳食纤维加分上限 6g score Math.max(0, Math.min(100, score)); return (int) score; }评分因素影响方式含义初始分 60基准值让普通水果落在及格线附近每 100g 热量超过 100 kcal 不继续加分热量越低得分越高每 100g 糖分每 1g 扣 2 分高糖水果明显降分膳食纤维每 1g 加 2 分6g 封顶纤维高的加分更多这个模型公开了一个很明确的逻辑热量低、糖分少、纤维多的水果分数高。答辩时老师如果问“评分依据是什么”可以直接答“权重表在代码里就是三个常量换成营养学标准只需要改这些系数”。如果论文里要写得更学术可以升级成层次分析法或模糊综合评价权重表就是现成的判断矩阵这是一条完整的扩展路径。4.4 收藏功能SharedPreferences 里 StringSet 的正确读写收藏是毕业设计中最容易出 Bug 的功能问题往往不在业务逻辑而在 SharedPreferences.getStringSet 的返回对象不能直接修改。官方返回的 Set 只是当前 Preference 文件底层集合的包装直接 add 不一定有异常但也不会写进去。private static final String PREFS_NAME fruit_fav; private static final String KEY_FAV fav_ids; public static SetString loadFav(Context context) { SharedPreferences sp context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); return sp.getStringSet(KEY_FAV, new HashSet()); } public static void saveFav(Context context, SetString favIds) { SharedPreferences sp context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); sp.edit().putStringSet(KEY_FAV, new HashSet(favIds)).apply(); }正确的写入套路是先把已有 Set copy 到一个新 HashSet改完这个副本后整体 putStringSet。这样第一次读、改动、回写三条链路都在安全边界内。收藏 id 用字符串是因为 StringSet 的泛型只支持 String存 int 要先转字符串。SetString favIds new HashSet(FavStore.loadFav(this)); if (favIds.contains(fruitId)) { favIds.remove(fruitId); } else { favIds.add(fruitId); } FavStore.saveFav(this, favIds);如果还要记录收藏时间、按时间排序StringSet 就不够用了。常见做法是直接存一个 JSON 数组字符串每条记录带 id 和 timestamp用 Gson 序列化后放进 putString 即可。这里在答辩时值得主动讲出来“收藏历史为什么要这么设计”比收藏本身更能体现实力。5. Android 答辩演示前要补的三件事三态页面、内存控制与演示动线5.1 三态页面用 ViewAnimator 切换 Loading、Empty 与 Error数据加载不管多稳都要考虑失败界面。安上 ViewAnimator按顺序放四层内容加载转圈、空数据提示、错误提示、RecyclerView用 setDisplayedChild 直接切到对应下标。ViewAnimator stateView findViewById(R.id.view_state); // 0 Loading1 Empty2 Error3 Content stateView.setDisplayedChild(list.isEmpty() ? 1 : 3);这个组件不需要额外依赖布局里一次性配好运行期只切下标比反复 setVisibility 干净得多。现场断网、数据文件损坏、搜索无结果三个场景都能对应到一个有界面的状态而不是白屏。5.2 内存与适配的两个检查点列表图先统一尺寸再显示。Glide 加载远程图时用 override 固定到 400x400 并 centerCrop避免不同水果图片原始分辨率差异导致内存抖动本地 drawable 也要准备同一尺寸的图不要拿一张几 MB 的素材直接塞进 item。布局层级控制在三层以内item 根布局用 ConstraintLayout减少 measurement 次数。演示前在最低配的模拟器上滑一遍列表图片能流畅滚动基本就到验收线了。5.3 演示动线先把答案留给提问演示动作对应代码被追问的方向输入“苹”看列表实时过滤TextWatcher contains中文匹配怎么做要不要分词筛选热量不超过 80 kcalapplyFilter多条件怎么合并0 值怎么处理进入详情看健康指数calcHealthyScore分数怎么算的权重依据收藏后杀掉进程重启StringSet 读写为什么用 SharedPreferences不用数据库演示时先搜索收窄列表再进详情讲计算函数完整走一遍用到的代码就是上面四个小节里的四段每一段都能对应一个答辩追问。老师打断你的时候停在哪个代码块都能往下讲。本文还有配套的精品资源点击获取
返回列表