
简介这是一份面向安卓开发学习者和毕业设计选题的智能饮食推荐App Demo源码核心围绕“数据分析图像识别”实现个性化饮食管理。项目不仅覆盖用户健康档案、菜品卡路里/蛋白质/脂肪数据还针对九体体质与减脂、增肌、塑形等目标联动推荐菜谱和运动方案并接入百度菜品识别API、Kaggle营养数据集及药膳/菜谱源具备卡路里累计、每日可视化和动态调整一周计划等能力。资源共286个文件压缩包仅4.72MB以95个Java源文件、83个XML布局/配置、24个Python脚本为主体辅以图片、JAR库、CSV数据集等兼顾Android界面逻辑、业务代码与数据脚本目录结构清晰适合整体研读与二次扩展。已有627人学习下载代码按功能模块分包可用于课程设计、毕业设计或健康类产品原型也为想了解图像识别与饮食推荐结合的开发者提供完整可运行参考。1. 这个饮食推荐 Demo,难点不在推荐算法,而在数据怎么变成可计算的拿到这个安卓端的智能饮食推荐项目,第一反应是推荐逻辑怎么写,实际上看完代码结构会发现,真正的技术重心在数据集处理和图像识别接入。项目里塞了一堆 CSV 文件,包括菜谱分类、食材营养、职业分类、体质分类,数据来源覆盖 Kaggle 的 OpenFoodFacts 和 McDonalds 营养表,再配合百度菜品识别 API 做拍照算卡路里。这个思路很实在:先有靠谱的食材营养数据,才有资格谈推荐。适合正在做毕业设计、或者想搭一个饮食健康类 APP 数据层的开发者参考,里面关于九体模式、动态周计划、防抖识别取舍和卡路里累加器的设计,能直接迁移到其它垂直类应用去。亮点是数据层做得比大多数课程设计厚实,坑也集中在数据上,比如 TSV 带 BOM、列名混乱、单位不统一,这些恰恰是面试时可以展开讲的点。2. 数据集选型与 CSV 预处理的工程化思路2.1 数据集选型:OpenFoodFacts、McDonalds 营养表、博食网三者怎么分工这个 Demo 在数据源上有三条线,分别解决不同层级的营养信息需求。第一条线是 OpenFoodFacts 的 TSV 全量包,字段覆盖上千种预包装食品的卡路里、蛋白质、脂肪、碳水、钠等,优点是开放可离线,缺点是体积大、列多、噪音多,适合做底层食材库的基础表。第二条线是 McDonalds 营养表,只有几十种快餐单品,但胜在字段干净、分量定义清晰(每份、每 100g),适合做套餐推荐的基准数据。第三条线是博食网(boohee.com)的动态爬虫,用于补齐中餐菜品和家常食材的卡路里信息,Kaggle 数据对中餐是盲区,不爬也就没有中国菜品维度。实际操作时,我的建议是:把 OpenFoodFacts 的 TSV 作为原始素材,不直接导入生产库,先做一次列裁剪和单位归一,抽成一份干净的food_nutrition.csv。McDonalds 数据做字典表,用于校验字段一致性。博食网爬虫作为增量补充,定期跑一次全量,而不是每次请求时实时爬,不然在高并发场景下必定被限流。这个分层在 Demo 里的体现是new_menu_names.csv负责菜单名称归一,menu_classification.csv负责菜品分类,occupation_classification.csv负责职业维度,拆分逻辑是清晰的。2.2 预处理:先看文件,再动手写清洗脚本从压缩包里看到这些 CSV 文件名带空格和括号,比如menu_classification (1).csv,这说明原始文件很可能从某平台二次导出或者手工二次编辑过。遇到这种情况,第一步不是直接读,而是先看文件头和行数,确定编码、分隔符、是否带 BOM。用file命令做初步检查:file -i en.openfoodfacts.org.products.tsv # 输出示例: text/tab-separated-values; charsetutf-8 head -n 3 en.openfoodfacts.org.products.tsv | cut -f1,8,9这一步能看到 TSV 是否真的是 tab 分隔。OpenFoodFacts 全量包常见问题是有少量行内嵌了换行符,用普通读取会直接错行。我一般会用 Python 配合csv模块的excel-tab方言来做容错处理:import csv def parse_tsv_safe(path, usecolsNone): with open(path, r, encodingutf-8-sig, newline) as f: reader csv.DictReader(f, delimiter\t) for row in reader: if usecols: row {k: row.get(k, ) for k in usecols} yield rowutf-8-sig编码用于剥掉 BOM,delimiter\t指定制表符,newline防止空行被错误解析。这里的usecols参数如果传入,则输出只保留你所关心的字段,在数据量大时可以显著减少内存占用。实际处理时还要注意数值列里的特殊值,比如卡路里字段可能缺失、为 0、或带单位,需要单独清洗。预处理脚本的逻辑整体分三段:列过滤、单位归一、异常值打标。列过滤是保留code、product_name、energy-kcal_100g、proteins_100g、fat_100g、carbohydrates_100g、salt_100g这些核心列;单位归一需要把kJ转kcal,OpenFoodFacts 原始数据里有energy-kj_100g和energy-kcal_100g两个字段,优先用后者;异常值处理则是对负数、超过 900 kcal/100g 的明显错误值打上-1标记,不让脏数据污染后续的推荐计算。2.3 列裁剪后入库:CSV 只是中间层,SQLite 才是推荐计算的底层清洗后的 CSV 不直接参与 APP 运行时查询。安卓端会把裁剪后的数据打进 Assets,首次启动时导入 SQLite。建表语句要有明确的主键和索引设计:CREATE TABLE IF NOT EXISTS food_nutrition ( code TEXT PRIMARY KEY, product_name TEXT, energy_kcal REAL, proteins REAL, fat REAL, carbs REAL, is_processed INTEGER DEFAULT 0 ); CREATE INDEX idx_food_name ON food_nutrition(product_name);建表的要点:code使用 TEXT 类型是因为 OpenFoodFacts 的条码有前导零,用 INTEGER 会丢精度;is_processed标记预包装食品与中餐食材,方便在推荐时区分「每 100g 恒定营养」和「每份按量估算」。索引建在product_name上,是因为用户输入法匹配查询占总查询量的 80% 以上,单列索引能应对。这份 SQLite 数据库还承担卡路里累加器的读取操作,每次用户记录一餐,查询只走这一张表,关联推荐表通过menu_classification.csv里的分类 ID 来做 join,性能瓶颈主要来自首屏导入,建议在异步线程分批插入。提示:如果直接从 CSV 全量灌入 SQLite 不做列裁剪,OpenFoodFacts 单表 300 万行会占超过 1GB 存储,导入时间分钟级,移动端完全不可接受。3. 百度菜品识别 API 的接入与卡路里估算策略3.1 鉴权、请求结构与 token 缓存百度菜品识别属于百度 AI 开放平台的图像识别服务,端点是https://aip.baidubce.com/rest/2.0/image-classify/v1/dish,采用 OAuth 2.0 的 client credentials 流程,先用 API Key 和 Secret Key 换取 access_token,再带 token 调识别接口。token 有效期 30 天,不能每次都重新申请,需要在本地缓存。Demo 里使用 OkHttp 做网络请求,Interceptor 统一附加 token 是一个常见做法:public class AuthInterceptor implements Interceptor { private String accessToken; Override public Response intercept(Chain chain) throws IOException { Request original chain.request(); HttpUrl url original.url().newBuilder() .addQueryParameter(access_token, accessToken) .build(); Request request original.newBuilder().url(url).build(); return chain.proceed(request); } }accessToken在应用启动时从 SharedPreferences 读取,如果为空或距过期时间不足一天,则发起一次 token 刷新请求,刷新成功后再写入持久化存储。这里必须处理并发请求时的 token 刷新竞争,最简单的做法是给 token 加载过程加synchronized块,保证只有一个线程去刷新。addQueryParameter是拼接鉴权参数的标准方式,放在拦截器里比散落在各个业务代码中更利于维护。3.2 识别请求参数与防抖设计识别接口的行为以表单形式 POST 图片二进制,核心参数是image和top_num。top_num控制返回前几个候选菜品,默认是 1,当识别结果置信度不高时可以返回 top 5 让用户确认。Demo 里还做了防抖处理:用户拍照后 800ms 内不重复发起请求,避免连拍触发多次计费调用。private long lastDishRequestTime 0L; private static final long DEBOUNCE_INTERVAL_MS 800L; private boolean allowRequest() { long current System.currentTimeMillis(); if (current - lastDishRequestTime DEBOUNCE_INTERVAL_MS) { return false; } lastDishRequestTime current; return true; }这里的防抖不是网络层的节流,而是交互层的策略,防止用户手滑连续点击「拍照识别」按钮。DEBOUNCE_INTERVAL_MS设置为 800ms 的原因是人手指双击间隔通常在 300ms 以内,800ms 能有效拦截意外触发,同时不让人觉得卡顿。allowRequest()在拍照回调里第一行调用,返回 false 就直接 return,不弹 Toast,静默过滤,这个细节对用户体验影响很大,频繁弹提示反而更烦。3.3 识别结果解析与卡路里换算公式百度返回的 JSON 结构包含result数组,每个元素有name(菜品名)、calorie(每 100g 卡路里)、probability(置信度)。这里的calorie是百度模型基于图片预估的,精度有限,所以不能直接累加,还需要结合用户选择的「份量」来换算。份量用枚举控制:小份 150g、中份 250g、大份 400g,换算逻辑如下:double caloriePer100g dishResult.getCalorie(); double weightGrams portionEnum.getWeightGrams(); double totalCalorie caloriePer100g * weightGrams / 100.0;这个公式是整条数据链最关键的一环:caloriePer100g来自识别接口,weightGrams来自用户选择,/100.0做百分比换算。如果识别接口没有返回卡路里字段,就降级用本地 SQLite 按菜品名查找,命中不了再让用户手动输入。推荐逻辑里用到的卡路里目标,也是在这个数值上做的累加和差值计算。另外要保留name和probability两个字段到本地记录表,方便后续展示「识别菜名 置信度」,同时可以基于历史信心值调整默认份量。3.4 识别率不够时的降级策略百度的菜品识别对中餐常见菜(西红柿炒蛋、宫保鸡丁)识别率尚可,但遇到复杂摆盘、多菜同盘就会乱。Demo 的做法是降级到二级选择界面:识别返回 top 3,用户点选一个,如果都不对,就进入搜索模式,走本地 SQLite 的product_nameLIKE 查询。这个降级链路必须在 UI 层做顺滑切换,不能因为识别失败就阻断流程。4. 双模式推荐结构与卡路里累加器的动态调整4.1 九体模式与普通健身模式的分野项目摘要里写了两种模式:填写额外信息(工作、运动时间、睡眠、九体、病史)的用户走「九体模式」,没填的走「普通健身模式」。普通健身模式按目标分为减肥、增肌、塑形、保持,推荐逻辑是「目标卡路里区间 三大营养素配比」;九体模式则叠加中医体质维度,推荐逻辑是「体质适配菜品 禁忌食材过滤 药膳周期插入」。两种模式共用同一套菜谱和食材数据表,差别在过滤器和评分器。普通模式按热量差排序,九体模式按体质适配度评分。建议实现两个接口:public interface DishRecommender { ListDish recommend(RecommendContext context); } public class FitnessRecommender implements DishRecommender { Override public ListDish recommend(RecommendContext context) { // 按卡路里区间过滤 蛋白质排序 } } public class ConstitutionRecommender implements DishRecommender { Override public ListDish recommend(RecommendContext context) { // 按体质适配度评分 禁忌食材排除 } }这里用接口做策略模式,方便后续扩展孕妇、糖尿病等更多垂直场景。RecommendContext里携带用户身高体重年龄、运动时长、体质类型、当前累计卡路里,这些字段从本地数据库读,不依赖服务端。FitnessRecommender先计算 BMR(基础代谢率),再叠加活动系数得到每日消耗,再根据增肌/减脂目标做 ±300 kcal 的偏移,用来约束推荐菜品的总热量上限。4.2 周计划动态生成与修正推荐菜谱分三层:单品推荐、组合推荐、一周大菜谱。一周大菜谱不是静态生成一次就完事,而是要响应用户当前的饮食记录。按摘要里的设计,每天用户拍照识别后,系统会动态调整后续几天的推荐。实现上,用一张weekly_plan_detail表维护每天的餐次记录:CREATE TABLE weekly_plan_detail ( plan_date TEXT NOT NULL, meal_type TEXT NOT NULL, dish_code TEXT, calorie REAL, status INTEGER DEFAULT 0, PRIMARY KEY (plan_date, meal_type, dish_code) );plan_date存储yyyy-MM-dd格式日期字符串,meal_type取值 breakfast / lunch / dinner / snack,status标记该餐是否已食用。每次识别完一餐并写入记录后,重新计算当天剩余卡路里预算,再刷新未来三天的推荐,表中按(plan_date, meal_type)做覆盖更新。这样用户当天吃多了,明天推荐的热量会相应降低,动态修正逻辑非常清晰。修正算法:每日剩余预算 当日目标摄入 - 当日已摄入累计值,若为负数则顺延到次日扣减,周内总预算保持动态平衡。这样用户某一天吃超标不会影响整体目标的结算。4.3 AIDL 接口与手环计步数据跨进程获取项目里出现了ISportStepInterface.aidl,这说明 Demo 把计步数据设计成了跨进程接口。.aidl文件定义 Binder 接口,用于从运动健康类应用(如手机厂商的计步服务)获取步数。AIDL 的使用边界要清楚:同一进程内直接用函数调用即可,跨进程才需要 AIDL。在饮食推荐场景里,步数用于计算运动消耗,进而影响推荐热量。定义一个可供调用的接口:package com.example.dietdemo; interface ISportStepInterface { int getTodaySteps(); int getYesterdaySteps(); }int返回值代表步数,在跨进程调用时基本类型可以直接传输,如果要传复杂对象才需要Parcelable。调用侧通过bindService绑定远端服务,在ServiceConnection.onServiceConnected里拿到ISportStepInterface的代理,然后调用getTodaySteps()获取数据。注意:跨进程接口的调用是同步阻塞的,Binder 默认的单向方法会阻塞调用线程,所以不要在 UI 线程直接调getTodaySteps(),需要放在子线程或者改用oneway定义。4.4 卡路里累加器与可视化数据准备累加器属于典型的写多读多场景:用户每记录一餐就写一次,周计划生成要读,可视化图表也要读。方案是内存缓存 SQLite 落盘。内存维护一个当天的累计值,每次写入后更新;可视化查询则直接查daily_consumption表的聚合结果:SELECT plan_date, SUM(calorie) AS total_calorie, SUM(proteins) AS total_protein, SUM(fat) AS total_fat FROM meal_record WHERE plan_date date(now, -7 days) GROUP BY plan_date ORDER BY plan_date;date(now, -7 days)是 SQLite 的日期函数,精确到自然日;SUM(calorie)返回数值可能有浮点误差,在前端展示时四舍五入到整数。这个查询结果用于折线图展示近七天卡路里变化,以及三大营养素柱状对比。可视化数据在 Demo 里用的是 MPAndroidChart 库,不要求实时刷新,只在onResume里重新查询一次即可。九体模式的特殊之处在于,还要按「体质适配度」过滤菜品,比如痰湿质要减少油腻食材的推荐频率,这在 SQL 层面可以用food_tags关联表实现,在推荐逻辑里做排除。5. 把 CSV 变成可查询的 SQLite:卡路里模糊查询的完整实现5.1 从 Assets 导入到 SQLite 的批处理写法前文提到把清洗后的 CSV 打进 Assets,这里补完整导入逻辑。安卓端不能直接对 Assets 里的文件做 SQL,要先拷贝到应用私有目录再 open,或者用流逐行读取插入 SQLite。数据量在几万行以内时,建议用SQLiteStatement批量插入:SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); String sql INSERT OR REPLACE INTO food_nutrition (code, product_name, energy_kcal, proteins, fat, carbs, is_processed) VALUES (?, ?, ?, ?, ?, ?, ?); SQLiteStatement statement db.compileStatement(sql); try (BufferedReader reader new BufferedReader(new InputStreamReader(context.getAssets().open(food_nutrition.csv)))) { String line; reader.readLine(); // 跳过表头 while ((line reader.readLine()) ! null) { String[] cols line.split(,, -1); // -1 保留空字段 statement.clearBindings(); statement.bindString(1, cols[0].trim()); statement.bindString(2, cols[1].trim()); statement.bindDouble(3, parseDoubleSafe(cols[2])); statement.bindDouble(4, parseDoubleSafe(cols[3])); statement.bindDouble(5, parseDoubleSafe(cols[4])); statement.bindDouble(6, parseDoubleSafe(cols[5])); statement.bindLong(7, Integer.parseInt(cols[6].trim())); statement.executeInsert(); } } db.setTransactionSuccessful(); db.endTransaction();beginTransaction()和setTransactionSuccessful()配合使用,全部插入成功才提交,否则自动回滚,避免导入半截导致脏数据。String.split(,, -1)的-1参数让末尾空列也保留,防止字段被丢弃导致索引错位。parseDoubleSafe是自定义方法,捕获NumberFormatException返回 0.0,应对 CSV 中的空值。5.2 卡路里查询的边界情况与索引优化用户输入菜品名时,LIKE查询会遇到中文和大小写问题。中文菜品名没有大小写问题,但需要处理全角空格和常见别名。menu_classification.csv提供分类,但查询逻辑还要考虑「鸡蛋炒西红柿」和「西红柿炒鸡蛋」这种顺序颠倒的菜名。Demo 里的做法是录入时同时存储菜品名和「关键词表」,关键词包含别名,查询时product_name IN (?, ?)或LIKE % || ? || %:SELECT product_name, energy_kcal, proteins, fat, carbs FROM food_nutrition WHERE product_name LIKE % || ? || % OR product_name IN ( SELECT name FROM dish_alias WHERE alias_name ? ) LIMIT 20;dish_alias是别名映射表,解决同菜不同名的问题。LIKE %关键词%在数据量几万行时用索引会失效,因为前置通配符导致全表扫描,但 Demo 场景数据量不大,单次扫描耗时毫秒级,完全可以接受。如果数据量到了百万级,就需要引入 FTS4/FTS5 全文搜索,这一点适合在性能优化阶段考虑。5.3 周计划持久化的幂等 upsert周计划表不能每次生成就删除重建,不然用户标记过的「已食用」状态会丢失。需要用INSERT OR REPLACE做幂等写入,主键用(plan_date, meal_type, dish_code)保证同一天同一餐同一道菜只存在一条记录。更新时判断:ContentValues values new ContentValues(); values.put(plan_date, date); values.put(meal_type, mealType); values.put(dish_code, dishCode); values.put(calorie, calorie); values.put(status, 1); int rows db.update(weekly_plan_detail, values, plan_date? AND meal_type? AND dish_code?, new String[]{date, mealType, dishCode}); if (rows 0) { db.insert(weekly_plan_detail, null, values); }先 update 后 insert,这是一种手动的 upsert 策略,比INSERT OR REPLACE更安全,因为不会因主键冲突删除再插入而触发连级操作。rows 0表示没有匹配记录,执行插入;否则是更新状态。这里status字段的含义是 0-未食用,1-已食用,2-跳过。推荐算法在生成次日计划时,对 status2 的餐次会补充替代推荐,保证每日总热量仍然落在目标区间。整个链路从「数据集选型 → CSV 清洗 → 百度识别 → 卡路里累加 → 双模式推荐 → 周计划持久化」已经闭合,数据在每一层的流转都有明确的结构和边界。Demo 最有参考价值的不是 UI,而是这套数据驱动推荐的骨架,把食材数据、识别结果、用户反馈三者串成了可迭代的系统。本文还有配套的精品资源点击获取