鸿蒙app 饮食营养计算(卡路里/营养成分分析)
【摘要】 引言科学饮食是健康管理的重要环节,用户需精准掌握每日摄入的卡路里与营养成分。鸿蒙系统的分布式能力、本地数据存储及多媒体交互特性,为饮食营养计算App提供了高效开发基础,支持食物搜索、营养成分分析及膳食计划推荐,助力用户实现个性化营养管理。技术背景鸿蒙框架:基于Stage模型,@Component构建UI,Search组件实现食物检索,Preferences存储用户偏好,关系型数据库缓存食物营...
引言
技术背景
-
鸿蒙框架:基于Stage模型, @Component构建UI,Search组件实现食物检索,Preferences存储用户偏好,关系型数据库缓存食物营养数据。 -
营养数据分析:内置常见食物营养数据库(如中国食物成分表),支持按克重计算热量、蛋白质、脂肪等核心指标。 -
可视化呈现:通过图表库(如 @ohos/chart)展示宏量营养素占比(饼图)、每日摄入趋势(折线图)。 -
智能推荐:基于用户目标(减脂/增肌/维持)与已摄入数据,推荐符合热量预算的食物组合。
应用使用场景
-
日常饮食记录:用户扫描或搜索食物(如“苹果200g”),App自动计算热量与营养成分并累加至当日总摄入。 -
膳食计划制定:用户设定“减脂期每日1500kcal”,App推荐早餐(燕麦+鸡蛋)、午餐(鸡胸肉沙拉)等搭配方案。 -
特殊人群管理:糖尿病患者记录碳水化合物摄入,高血压患者追踪钠含量,辅助控制病情。
核心特性
-
精准营养计算:覆盖1000+常见食物,支持自定义食材重量与烹饪方式(生重/熟重)修正。 -
多维数据可视化:饼图展示三大营养素占比,日历标记每日达标情况,趋势图反映长期摄入变化。 -
智能推荐引擎:结合用户目标、过敏禁忌与剩余热量预算,生成个性化餐单。 -
离线可用:食物营养数据本地存储,无网络时仍可记录与分析。
原理流程图与原理解释
流程图
graph TD
A[用户输入食物名称/重量] --> B[本地数据库检索营养成分]
B --> C[计算实际摄入量(重量×单位营养值)]
C --> D[累加至当日总摄入]
D --> E[可视化展示:总热量/营养素占比/趋势]
A --> F[智能推荐:匹配目标与剩余预算]
F --> G[生成推荐餐单]
原理解释
-
营养计算:每种食物预存“每100g可食部”的营养参数(如热量、蛋白质、脂肪、碳水),用户输入重量后按比例换算(如200g苹果热量=200/100×52kcal=104kcal)。 -
数据检索:通过模糊搜索匹配食物名称(如“苹果”匹配“红富士苹果”“青苹果”),支持别名映射(如“番茄”=“西红柿”)。 -
可视化逻辑:从数据库读取当日各餐摄入数据,计算蛋白质/脂肪/碳水占总热量的百分比,驱动饼图渲染;按日期聚合总热量生成折线图数据点。
环境准备
-
开发工具:DevEco Studio 4.0+ -
SDK版本:API 9+(支持数据库、图表库、搜索组件) -
权限配置:在 module.json5中声明权限(本示例主要涉及本地数据,无需额外权限):"requestPermissions": [] -
资源准备:在 entry/src/main/resources/rawfile下存放食物营养数据JSON(如food_nutrition.json),格式示例:[ {"id":1,"name":"苹果","alias":["红富士"],"calorie":52,"protein":0.3,"fat":0.2,"carb":14,"unit":"100g"}, {"id":2,"name":"鸡胸肉","alias":[],"calorie":133,"protein":19.4,"fat":5,"carb":2.5,"unit":"100g"} ]
代码实现(完整示例)
1. 数据模型(Model/NutritionData.ts)
// 食物营养信息
export class FoodNutrition {
id: number;
name: string; // 食物名称
alias: string[]; // 别名
calorie: number; // 每100g热量(kcal)
protein: number; // 每100g蛋白质(g)
fat: number; // 每100g脂肪(g)
carb: number; // 每100g碳水(g)
unit: string; // 计量单位
constructor(id: number, name: string, alias: string[], calorie: number, protein: number, fat: number, carb: number, unit: string) {
this.id = id;
this.name = name;
this.alias = alias;
this.calorie = calorie;
this.protein = protein;
this.fat = fat;
this.carb = carb;
this.unit = unit;
}
}
// 单条饮食记录
export class DietRecord {
id: number;
foodId: number;
foodName: string;
weight: number; // 摄入重量(g)
calorie: number; // 实际摄入热量
protein: number; // 实际摄入蛋白质
fat: number; // 实际摄入脂肪
carb: number; // 实际摄入碳水
mealType: string; // 餐别(早餐/午餐/晚餐)
recordTime: string; // 记录时间(yyyy-MM-dd HH:mm)
constructor(id: number, foodId: number, foodName: string, weight: number, calorie: number, protein: number, fat: number, carb: number, mealType: string) {
this.id = id;
this.foodId = foodId;
this.foodName = foodName;
this.weight = weight;
this.calorie = calorie;
this.protein = protein;
this.fat = fat;
this.carb = carb;
this.mealType = mealType;
this.recordTime = new Date().toISOString().replace('T', ' ').substring(0, 16);
}
}
2. 食物数据库(Database/FoodDB.ts)
import relationalStore from '@ohos.data.relationalStore';
import { FoodNutrition } from '../Model/NutritionData';
import fs from '@ohos.file.fs';
export class FoodDB {
private rdbStore: relationalStore.RdbStore | null = null;
private context: Context | null = null;
async init(context: Context) {
this.context = context;
this.rdbStore = await relationalStore.getRdbStore(context, {
name: 'food_nutrition.db',
securityLevel: relationalStore.SecurityLevel.S1
});
await this.createTable();
await this.importInitialData(); // 首次启动时导入JSON数据
}
// 创建食物表
private async createTable() {
const sql = `CREATE TABLE IF NOT EXISTS food (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
alias TEXT, -- 存储为JSON数组字符串
calorie REAL,
protein REAL,
fat REAL,
carb REAL,
unit TEXT
)`;
await this.rdbStore!.executeSql(sql);
}
// 从rawfile导入初始数据
private async importInitialData() {
const file = fs.openSync(this.context!.resourceManager.getRawFd('food_nutrition.json'), fs.OpenMode.READ_ONLY);
const buffer = new ArrayBuffer(file.length);
fs.readSync(file.fd, buffer);
const jsonStr = String.fromCharCode.apply(null, new Uint8Array(buffer));
const foods: FoodNutrition[] = JSON.parse(jsonStr);
// 清空旧数据(仅首次导入)
const count = await this.getFoodCount();
if (count === 0) {
for (const food of foods) {
await this.rdbStore!.insert('food', {
'id': food.id,
'name': food.name,
'alias': JSON.stringify(food.alias),
'calorie': food.calorie,
'protein': food.protein,
'fat': food.fat,
'carb': food.carb,
'unit': food.unit
});
}
}
fs.closeSync(file);
}
private async getFoodCount(): Promise<number> {
const resultSet = await this.rdbStore!.query(new relationalStore.RdbPredicates('food'), ['COUNT(*) as count']);
resultSet.goToNextRow();
const count = resultSet.getLong(resultSet.getColumnIndex('count'));
resultSet.close();
return count;
}
// 搜索食物(支持名称/别名模糊匹配)
async searchFood(keyword: string): Promise<FoodNutrition[]> {
const foods: FoodNutrition[] = [];
// 精确匹配名称
let predicates = new relationalStore.RdbPredicates('food').like('name', `%${keyword}%`);
let resultSet = await this.rdbStore!.query(predicates, ['*']);
this.parseResultSet(foods, resultSet);
// 匹配别名
predicates = new relationalStore.RdbPredicates('food').contains('alias', keyword);
resultSet = await this.rdbStore!.query(predicates, ['*']);
this.parseResultSet(foods, resultSet);
return foods;
}
private parseResultSet(foods: FoodNutrition[], resultSet: relationalStore.ResultSet) {
while (resultSet.goToNextRow()) {
const aliasStr = resultSet.getString(resultSet.getColumnIndex('alias'));
foods.push(new FoodNutrition(
resultSet.getLong(resultSet.getColumnIndex('id')),
resultSet.getString(resultSet.getColumnIndex('name')),
JSON.parse(aliasStr || '[]'),
resultSet.getDouble(resultSet.getColumnIndex('calorie')),
resultSet.getDouble(resultSet.getColumnIndex('protein')),
resultSet.getDouble(resultSet.getColumnIndex('fat')),
resultSet.getDouble(resultSet.getColumnIndex('carb')),
resultSet.getString(resultSet.getColumnIndex('unit'))
));
}
resultSet.close();
}
// 根据ID获取食物详情
async getFoodById(id: number): Promise<FoodNutrition | null> {
const predicates = new relationalStore.RdbPredicates('food').equalTo('id', id);
const resultSet = await this.rdbStore!.query(predicates, ['*']);
let food: FoodNutrition | null = null;
if (resultSet.goToNextRow()) {
const aliasStr = resultSet.getString(resultSet.getColumnIndex('alias'));
food = new FoodNutrition(
resultSet.getLong(resultSet.getColumnIndex('id')),
resultSet.getString(resultSet.getColumnIndex('name')),
JSON.parse(aliasStr || '[]'),
resultSet.getDouble(resultSet.getColumnIndex('calorie')),
resultSet.getDouble(resultSet.getColumnIndex('protein')),
resultSet.getDouble(resultSet.getColumnIndex('fat')),
resultSet.getDouble(resultSet.getColumnIndex('carb')),
resultSet.getString(resultSet.getColumnIndex('unit'))
);
}
resultSet.close();
return food;
}
}
3. 饮食记录数据库(Database/DietLogDB.ts)
import relationalStore from '@ohos.data.relationalStore';
import { DietRecord } from '../Model/NutritionData';
export class DietLogDB {
private rdbStore: relationalStore.RdbStore | null = null;
async init(context: Context) {
this.rdbStore = await relationalStore.getRdbStore(context, {
name: 'diet_log.db',
securityLevel: relationalStore.SecurityLevel.S1
});
const sql = `CREATE TABLE IF NOT EXISTS diet_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
food_id INTEGER,
food_name TEXT,
weight REAL,
calorie REAL,
protein REAL,
fat REAL,
carb REAL,
meal_type TEXT,
record_time TEXT
)`;
await this.rdbStore.executeSql(sql);
}
// 添加饮食记录
async addRecord(record: Omit<DietRecord, 'id' | 'recordTime'>): Promise<boolean> {
if (!this.rdbStore) return false;
const valueBucket = {
'food_id': record.foodId,
'food_name': record.foodName,
'weight': record.weight,
'calorie': record.calorie,
'protein': record.protein,
'fat': record.fat,
'carb': record.carb,
'meal_type': record.mealType,
'record_time': new Date().toISOString().replace('T', ' ').substring(0, 16)
};
try {
await this.rdbStore.insert('diet_log', valueBucket);
return true;
} catch (err) {
console.error(`Add diet log failed: ${err}`);
return false;
}
}
// 获取当日总摄入
async getTodayTotalIntake(): Promise<{ totalCalorie: number; totalProtein: number; totalFat: number; totalCarb: number }> {
if (!this.rdbStore) return { totalCalorie: 0, totalProtein: 0, totalFat: 0, totalCarb: 0 };
const today = new Date().toISOString().split('T')[0];
const predicates = new relationalStore.RdbPredicates('diet_log').like('record_time', `${today}%`);
const resultSet = await this.rdbStore.query(predicates, ['SUM(calorie) as total_calorie', 'SUM(protein) as total_protein', 'SUM(fat) as total_fat', 'SUM(carb) as total_carb']);
let totals = { totalCalorie: 0, totalProtein: 0, totalFat: 0, totalCarb: 0 };
if (resultSet.goToNextRow()) {
totals.totalCalorie = resultSet.getDouble(resultSet.getColumnIndex('total_calorie')) || 0;
totals.totalProtein = resultSet.getDouble(resultSet.getColumnIndex('total_protein')) || 0;
totals.totalFat = resultSet.getDouble(resultSet.getColumnIndex('total_fat')) || 0;
totals.totalCarb = resultSet.getDouble(resultSet.getColumnIndex('total_carb')) || 0;
}
resultSet.close();
return totals;
}
// 获取当日各餐记录
async getTodayRecordsByMeal(): Promise<Record<string, DietRecord[]>> {
if (!this.rdbStore) return {};
const today = new Date().toISOString().split('T')[0];
const predicates = new relationalStore.RdbPredicates('diet_log').like('record_time', `${today}%`);
const resultSet = await this.rdbStore.query(predicates, ['*']);
const records: DietRecord[] = [];
while (resultSet.goToNextRow()) {
records.push(new DietRecord(
resultSet.getLong(resultSet.getColumnIndex('id')),
resultSet.getLong(resultSet.getColumnIndex('food_id')),
resultSet.getString(resultSet.getColumnIndex('food_name')),
resultSet.getDouble(resultSet.getColumnIndex('weight')),
resultSet.getDouble(resultSet.getColumnIndex('calorie')),
resultSet.getDouble(resultSet.getColumnIndex('protein')),
resultSet.getDouble(resultSet.getColumnIndex('fat')),
resultSet.getDouble(resultSet.getColumnIndex('carb')),
resultSet.getString(resultSet.getColumnIndex('meal_type'))
));
}
resultSet.close();
// 按餐别分组
return records.reduce((acc, record) => {
if (!acc[record.mealType]) acc[record.mealType] = [];
acc[record.mealType].push(record);
return acc;
}, {} as Record<string, DietRecord[]>);
}
}
4. UI界面(pages/Index.ets)
import { FoodDB } from '../Database/FoodDB';
import { DietLogDB } from '../Database/DietLogDB';
import { FoodNutrition, DietRecord } from '../Model/NutritionData';
@Entry
@Component
struct NutritionCalculatorPage {
@State searchKeyword: string = '';
@State searchResults: FoodNutrition[] = [];
@State inputWeight: string = '100'; // 默认100g
@State selectedMeal: string = '早餐'; // 默认早餐
@State todayTotal: { totalCalorie: number; totalProtein: number; totalFat: number; totalCarb: number } = { totalCalorie: 0, totalProtein: 0, totalFat: 0, totalCarb: 0 };
@State mealRecords: Record<string, DietRecord[]> = {};
private foodDB: FoodDB = new FoodDB();
private dietLogDB: DietLogDB = new DietLogDB();
aboutToAppear() {
this.initDatabases();
this.loadTodayData();
}
async initDatabases() {
await this.foodDB.init(getContext());
await this.dietLogDB.init(getContext());
}
async loadTodayData() {
this.todayTotal = await this.dietLogDB.getTodayTotalIntake();
this.mealRecords = await this.dietLogDB.getTodayRecordsByMeal();
}
// 搜索食物
async onSearch() {
if (this.searchKeyword.trim()) {
this.searchResults = await this.foodDB.searchFood(this.searchKeyword.trim());
} else {
this.searchResults = [];
}
}
// 添加饮食记录
async addDietRecord(food: FoodNutrition) {
const weight = parseFloat(this.inputWeight) || 0;
if (weight <= 0) {
return;
}
// 计算实际摄入量(每100g营养值 × 重量/100)
const calorie = food.calorie * weight / 100;
const protein = food.protein * weight / 100;
const fat = food.fat * weight / 100;
const carb = food.carb * weight / 100;
const success = await this.dietLogDB.addRecord({
foodId: food.id,
foodName: food.name,
weight: weight,
calorie: calorie,
protein: protein,
fat: fat,
carb: carb,
mealType: this.selectedMeal
});
if (success) {
// 刷新数据
this.loadTodayData();
// 清空搜索
this.searchKeyword = '';
this.searchResults = [];
this.inputWeight = '100';
}
}
// 计算营养素占比(用于饼图)
getNutrientPercentages(): { protein: number; fat: number; carb: number } {
const totalCalorie = this.todayTotal.totalCalorie || 1; // 避免除零
// 蛋白质/碳水 4kcal/g,脂肪9kcal/g
const proteinCalorie = this.todayTotal.totalProtein * 4;
const fatCalorie = this.todayTotal.totalFat * 9;
const carbCalorie = this.todayTotal.totalCarb * 4;
return {
protein: Math.round((proteinCalorie / totalCalorie) * 100),
fat: Math.round((fatCalorie / totalCalorie) * 100),
carb: Math.round((carbCalorie / totalCalorie) * 100)
};
}
build() {
Column({ space: 20 }) {
// 标题
Text("饮食营养计算").fontSize(24).fontWeight(FontWeight.Bold).margin(16);
// 今日总摄入概览
Column({ space: 10 }) {
Text("今日总摄入").fontSize(18).fontWeight(FontWeight.Medium);
Row() {
Text(`热量: ${this.todayTotal.totalCalorie.toFixed(1)} kcal`).fontSize(16);
Text(`蛋白质: ${this.todayTotal.totalProtein.toFixed(1)} g`).fontSize(16);
Text(`脂肪: ${this.todayTotal.totalFat.toFixed(1)} g`).fontSize(16);
Text(`碳水: ${this.todayTotal.totalCarb.toFixed(1)} g`).fontSize(16);
}.justifyContent(FlexAlign.SpaceAround).width('100%');
}.width('100%').padding(10).backgroundColor('#F5F5F5').borderRadius(8);
// 搜索食物
Row() {
Search({ placeholder: "输入食物名称(如苹果)" })
.layoutWeight(1)
.onChange((value: string) => this.searchKeyword = value)
.onSubmit(() => this.onSearch());
Button("搜索").onClick(() => this.onSearch());
}.width('100%').margin({ top: 10 });
// 搜索结果列表
if (this.searchResults.length > 0) {
List() {
ForEach(this.searchResults, (food: FoodNutrition) => {
ListItem() {
Row() {
Column() {
Text(food.name).fontSize(16);
Text(`${food.calorie} kcal/100g`).fontSize(14).fontColor(Color.Gray);
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Button("添加").onClick(() => this.addDietRecord(food));
}.width('100%').padding(10)
}
})
}.width('100%').height(200).margin({ top: 10 });
// 输入重量与餐别
Row() {
Text("重量(g):").fontSize(16);
TextInput({ text: this.inputWeight })
.layoutWeight(1)
.type(InputType.Number)
.onChange((value: string) => this.inputWeight = value);
}.width('100%').margin({ top: 10 });
Row() {
Text("餐别:").fontSize(16);
Select([{ value: '早餐' }, { value: '午餐' }, { value: '晚餐' }, { value: '加餐' }])
.selected(this.selectedMeal)
.onSelect((index: number, value: string) => this.selectedMeal = value)
.layoutWeight(1);
}.width('100%').margin({ top: 10 });
}
// 营养素占比饼图(示例用Text模拟,实际可替换为chart组件)
Column({ space: 10 }) {
Text("三大营养素占比").fontSize(18).fontWeight(FontWeight.Medium);
const percentages = this.getNutrientPercentages();
Row() {
Text(`蛋白质: ${percentages.protein}%`).fontSize(16).backgroundColor('#FF9800').padding(5).borderRadius(4);
Text(`脂肪: ${percentages.fat}%`).fontSize(16).backgroundColor('#F44336').padding(5).borderRadius(4);
Text(`碳水: ${percentages.carb}%`).fontSize(16).backgroundColor('#2196F3').padding(5).borderRadius(4);
}.justifyContent(FlexAlign.SpaceAround).width('100%');
}.width('100%').margin({ top: 20 });
// 今日各餐记录
Column({ space: 10 }) {
Text("今日饮食记录").fontSize(18).fontWeight(FontWeight.Medium);
ForEach(Object.keys(this.mealRecords), (mealType: string) => {
Column({ space: 5 }) {
Text(`${mealType}:`).fontSize(16).fontColor(Color.Blue);
ForEach(this.mealRecords[mealType], (record: DietRecord) => {
Row() {
Text(`${record.foodName} ${record.weight}g`).fontSize(14);
Text(`${record.calorie.toFixed(1)} kcal`).fontSize(14).fontColor(Color.Gray);
}.width('100%').padding(5).backgroundColor('#FAFAFA').borderRadius(4);
})
}
})
}.width('100%').margin({ top: 20 });
}
.width('100%').height('100%').padding(16)
}
}
运行结果与测试步骤
运行结果
-
搜索与添加:输入“苹果”搜索,列表显示“苹果 52 kcal/100g”,输入重量“200”,选择“早餐”,点击“添加”后,“今日总摄入”热量增加104kcal(52×200/100)。 -
数据可视化:营养素占比区域根据总摄入计算并显示蛋白质/脂肪/碳水的百分比(如蛋白质30%、脂肪40%、碳水30%)。 -
记录展示:各餐记录区按“早餐/午餐/晚餐”分组显示已添加的食物名称、重量与热量。
测试步骤
-
环境配置:创建鸿蒙工程,添加权限与代码文件,将 food_nutrition.json放入resources/rawfile。 -
模拟器运行:使用API 9+模拟器,运行App,验证首页加载正常。 -
搜索功能:输入“鸡胸肉”,确认搜索结果返回“鸡胸肉 133 kcal/100g”。 -
添加记录:输入重量“150”,选择“午餐”,点击“添加”,检查“今日总摄入”热量是否增加199.5kcal(133×150/100)。
部署场景
-
个人手机:作为日常饮食记录工具,离线使用,适合关注体重管理或慢性病患者。 -
智能冰箱:通过鸿蒙分布式能力,冰箱屏幕同步显示推荐菜谱与剩余热量预算。 -
医疗机构:医院营养科部署定制版,为患者提供个性化膳食指导与摄入监控。
疑难解答
-
搜索无结果:检查 food_nutrition.json中食物名称是否与搜索关键词匹配(区分大小写),确认数据库初始化成功(aboutToAppear中调用initDatabases)。 -
热量计算错误:验证食物中 calorie字段是否为“每100g”数值,确认计算公式重量/100×单位营养值正确。 -
图表不显示:若使用真实图表库,检查数据是否为空(如当日无记录时 totalCalorie为0,需特殊处理)。
未来展望与技术趋势与挑战
未来展望
-
图像识别:集成相机能力,用户拍摄食物照片即可自动识别种类并计算营养(需CV算法支持)。 -
血糖/血脂联动:对接智能穿戴设备数据,根据血糖波动动态调整碳水推荐量。 -
社区食谱共享:通过鸿蒙分布式数据共享,用户可分享低卡食谱并查看好友饮食打卡。
技术挑战
-
食物数据维护:需定期更新食物营养数据库(如新品种、加工方式差异),保证数据准确性。 -
个性化推荐复杂度:结合用户代谢率、运动量等多维度数据生成推荐,算法需持续优化。
总结
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)