
鸿蒙简易菜品热量计算应用开发指南 原创
鸿蒙简易菜品热量计算应用开发指南
一、系统架构设计
基于HarmonyOS的分布式能力和AI技术,我们设计了一套简易菜品热量计算系统,主要功能包括:
菜品拍照:使用设备相机拍摄菜品照片
AI识别:识别菜品成分和分量
热量估算:计算菜品总热量和营养成分
多设备同步:跨设备同步饮食记录和健康数据
健康建议:根据用户目标提供饮食建议
!https://example.com/harmony-food-calorie-arch.png
二、核心代码实现
相机服务管理
// FoodCameraService.ets
import camera from ‘@ohos.multimedia.camera’;
import image from ‘@ohos.multimedia.image’;
class FoodCameraService {
private static instance: FoodCameraService;
private cameraManager: camera.CameraManager;
private cameraInput: camera.CameraInput | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private photoOutput: camera.PhotoOutput | null = null;
private constructor() {
this.cameraManager = camera.getCameraManager();
public static getInstance(): FoodCameraService {
if (!FoodCameraService.instance) {
FoodCameraService.instance = new FoodCameraService();
return FoodCameraService.instance;
public async initCamera(previewSurfaceId: string): Promise<void> {
const cameras = this.cameraManager.getSupportedCameras();
if (cameras.length === 0) {
throw new Error('No camera available');
this.cameraInput = this.cameraManager.createCameraInput(cameras[0]);
await this.cameraInput.open();
const previewProfile = this.cameraManager.getSupportedOutputCapability(
cameras[0],
camera.ProfileMode.PROFILE_MODE_DEFAULT
).previewProfiles[0];
this.previewOutput = this.cameraManager.createPreviewOutput(
previewProfile,
previewSurfaceId
);
const photoProfiles = this.cameraManager.getSupportedOutputCapability(
cameras[0],
camera.ProfileMode.PROFILE_MODE_DEFAULT
).photoProfiles;
this.photoOutput = this.cameraManager.createPhotoOutput(
photoProfiles[0]
);
public async takeFoodPhoto(): Promise<image.Image> {
if (!this.photoOutput) {
throw new Error('Photo output not initialized');
const photoSettings = {
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
rotation: camera.ImageRotation.ROTATION_0
};
return new Promise((resolve, reject) => {
this.photoOutput!.capture(photoSettings, (err, image) => {
if (err) {
reject(err);
else {
resolve(image);
});
});
}
export const foodCameraService = FoodCameraService.getInstance();
菜品识别服务
// FoodRecognitionService.ets
import image from ‘@ohos.multimedia.image’;
import http from ‘@ohos.net.http’;
class FoodRecognitionService {
private static instance: FoodRecognitionService;
private httpClient: http.HttpRequest;
private apiKey = ‘YOUR_FOOD_API_KEY’;
private constructor() {
this.httpClient = http.createHttp();
public static getInstance(): FoodRecognitionService {
if (!FoodRecognitionService.instance) {
FoodRecognitionService.instance = new FoodRecognitionService();
return FoodRecognitionService.instance;
public async recognizeFood(image: image.Image): Promise<FoodInfo> {
const formData = new FormData();
formData.append('image', image);
formData.append('api_key', this.apiKey);
return new Promise((resolve, reject) => {
this.httpClient.request(
'https://food-api.example.com/identify',
method: ‘POST’,
header: { 'Content-Type': 'multipart/form-data' },
extraData: formData
},
(err, data) => {
if (err) {
reject(err);
else {
const result = JSON.parse(data.result);
resolve(this.processFoodInfo(result));
}
);
});
private processFoodInfo(rawData: any): FoodInfo {
return {
name: rawData.name || '未知菜品',
ingredients: rawData.ingredients.map(ing => ({
name: ing.name,
amount: ing.amount,
unit: ing.unit,
calories: ing.calories,
protein: ing.protein,
fat: ing.fat,
carbs: ing.carbs
})),
totalCalories: rawData.totalCalories,
confidence: rawData.confidence
};
}
export const foodRecognitionService = FoodRecognitionService.getInstance();
多设备同步服务
// FoodSyncService.ets
import distributedData from ‘@ohos.data.distributedData’;
import deviceManager from ‘@ohos.distributedHardware.deviceManager’;
class FoodSyncService {
private static instance: FoodSyncService;
private kvManager: distributedData.KVManager;
private kvStore: distributedData.KVStore;
private constructor() {
this.initKVStore();
private async initKVStore(): Promise<void> {
const config = {
bundleName: 'com.example.foodCalorie',
userInfo: { userId: 'currentUser' }
};
this.kvManager = distributedData.createKVManager(config);
this.kvStore = await this.kvManager.getKVStore('food_store', {
createIfMissing: true
});
this.kvStore.on('dataChange', (data) => {
this.handleRemoteUpdate(data);
});
public static getInstance(): FoodSyncService {
if (!FoodSyncService.instance) {
FoodSyncService.instance = new FoodSyncService();
return FoodSyncService.instance;
public async syncMealRecord(record: MealRecord): Promise<void> {
await this.kvStore.put(meal_${record.id}, JSON.stringify(record));
public async getMealRecords(date: string): Promise<MealRecord[]> {
const entries = await this.kvStore.getEntries('meal_');
return Array.from(entries)
.map(([_, value]) => JSON.parse(value))
.filter(record => record.date === date);
public async syncUserProfile(profile: UserProfile): Promise<void> {
await this.kvStore.put('user_profile', JSON.stringify(profile));
public async getUserProfile(): Promise<UserProfile | null> {
const value = await this.kvStore.get('user_profile');
return value ? JSON.parse(value) : null;
private handleRemoteUpdate(data: distributedData.ChangeInfo): void {
if (data.deviceId === deviceInfo.deviceId) return;
const key = data.key as string;
if (key.startsWith('meal_')) {
const record = JSON.parse(data.value);
EventBus.emit('mealRecordUpdated', record);
else if (key === ‘user_profile’) {
const profile = JSON.parse(data.value);
EventBus.emit('userProfileUpdated', profile);
}
export const foodSyncService = FoodSyncService.getInstance();
热量计算服务
// CalorieCalculationService.ets
class CalorieCalculationService {
private static instance: CalorieCalculationService;
private constructor() {}
public static getInstance(): CalorieCalculationService {
if (!CalorieCalculationService.instance) {
CalorieCalculationService.instance = new CalorieCalculationService();
return CalorieCalculationService.instance;
public calculateTotalCalories(foodInfo: FoodInfo): number {
return foodInfo.ingredients.reduce(
(total, ing) => total + ing.calories,
);
public calculateDailyIntake(records: MealRecord[]): DailyIntake {
return records.reduce(
(daily, record) => {
daily.calories += record.totalCalories;
daily.protein += record.totalProtein;
daily.fat += record.totalFat;
daily.carbs += record.totalCarbs;
return daily;
},
calories: 0, protein: 0, fat: 0, carbs: 0 }
);
public getHealthAdvice(dailyIntake: DailyIntake, userProfile: UserProfile): HealthAdvice {
const targetCalories = this.calculateTargetCalories(userProfile);
const remainingCalories = targetCalories - dailyIntake.calories;
return {
remainingCalories,
suggestion: remainingCalories > 0
还可以摄入约${remainingCalories}大卡
已超出${-remainingCalories}大卡,
balancedDiet: this.getBalancedDietAdvice(dailyIntake, targetCalories)
};
private calculateTargetCalories(userProfile: UserProfile): number {
// 基础代谢率(BMR)计算
let bmr = userProfile.gender === 'male'
88.362 + (13.397 userProfile.weight) + (4.799 userProfile.height) - (5.677 * userProfile.age)
447.593 + (9.247 userProfile.weight) + (3.098 userProfile.height) - (4.330 * userProfile.age);
// 根据活动水平调整
const activityFactors = {
sedentary: 1.2,
lightlyActive: 1.375,
moderatelyActive: 1.55,
veryActive: 1.725,
extraActive: 1.9
};
bmr *= activityFactors[userProfile.activityLevel];
// 根据目标调整
if (userProfile.goal === 'lose') {
return bmr * 0.8; // 减重建议减少20%摄入
else if (userProfile.goal === ‘gain’) {
return bmr * 1.2; // 增重建议增加20%摄入
return bmr; // 维持体重
private getBalancedDietAdvice(dailyIntake: DailyIntake, targetCalories: number): string[] {
const advice = [];
const proteinPercent = (dailyIntake.protein * 4) / dailyIntake.calories;
const fatPercent = (dailyIntake.fat * 9) / dailyIntake.calories;
const carbsPercent = (dailyIntake.carbs * 4) / dailyIntake.calories;
if (proteinPercent < 0.1) {
advice.push('建议增加蛋白质摄入,如鸡胸肉、鱼类、豆制品等');
else if (proteinPercent > 0.35) {
advice.push('蛋白质摄入偏高,可适当减少');
if (fatPercent < 0.2) {
advice.push('建议增加健康脂肪摄入,如坚果、橄榄油、牛油果等');
else if (fatPercent > 0.35) {
advice.push('脂肪摄入偏高,可适当减少油炸食品和高脂肪食物');
if (carbsPercent < 0.45) {
advice.push('建议增加碳水化合物摄入,如全谷物、蔬菜、水果等');
else if (carbsPercent > 0.65) {
advice.push('碳水化合物摄入偏高,可适当减少精制糖和精制谷物');
return advice.length > 0 ? advice : [‘当前营养比例均衡,继续保持!’];
}
export const calorieService = CalorieCalculationService.getInstance();
三、主界面实现
相机识别界面
// FoodCameraView.ets
@Component
struct FoodCameraView {
@State previewSurfaceId: string = ‘’;
@State capturedImage: image.Image | null = null;
@State isRecognizing: boolean = false;
aboutToAppear() {
this.initCamera();
build() {
Stack() {
// 相机预览
XComponent({
id: 'foodCameraPreview',
type: 'surface',
libraryname: 'libcamera.so',
controller: this.previewController
})
.onLoad(() => {
this.previewSurfaceId = this.previewController.getXComponentSurfaceId();
foodCameraService.initCamera(this.previewSurfaceId);
})
.width('100%')
.height('100%')
// 拍照按钮
Button('识别菜品')
.onClick(() => this.captureAndRecognize())
.position({ x: '50%', y: '90%' })
// 识别状态
if (this.isRecognizing) {
LoadingProgress()
.position({ x: '50%', y: '50%' })
}
private async captureAndRecognize(): Promise<void> {
this.isRecognizing = true;
this.capturedImage = await foodCameraService.takeFoodPhoto();
const foodInfo = await foodRecognitionService.recognizeFood(this.capturedImage);
// 计算总热量
const totalCalories = calorieService.calculateTotalCalories(foodInfo);
// 保存识别记录
const record: MealRecord = {
id: generateId(),
foodInfo,
date: getCurrentDate(),
time: getCurrentTime(),
totalCalories,
totalProtein: foodInfo.ingredients.reduce((sum, ing) => sum + ing.protein, 0),
totalFat: foodInfo.ingredients.reduce((sum, ing) => sum + ing.fat, 0),
totalCarbs: foodInfo.ingredients.reduce((sum, ing) => sum + ing.carbs, 0)
};
await foodSyncService.syncMealRecord(record);
EventBus.emit('foodIdentified', foodInfo);
this.isRecognizing = false;
}
function generateId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
function getCurrentDate(): string {
const now = new Date();
return {now.getFullYear()}-{now.getMonth() + 1}-${now.getDate()};
function getCurrentTime(): string {
const now = new Date();
return {now.getHours()}:{now.getMinutes()};
菜品详情界面
// FoodDetailView.ets
@Component
struct FoodDetailView {
@State foodInfo: FoodInfo | null = null;
@State healthAdvice: HealthAdvice | null = null;
aboutToAppear() {
EventBus.on(‘foodIdentified’, (info) => {
this.foodInfo = info;
this.loadHealthAdvice();
});
build() {
Column() {
if (this.foodInfo) {
Scroll() {
Column() {
// 菜品基本信息
Text(this.foodInfo.name)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(总热量: ${this.foodInfo.totalCalories.toFixed(0)} 大卡)
.fontSize(18)
.margin({ top: 8 })
// 营养成分
Text('营养成分')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 24 })
Row() {
Column() {
Text('蛋白质')
.fontSize(16)
Text(${this.foodInfo.ingredients.reduce((sum, ing) => sum + ing.protein, 0).toFixed(1)}g)
.fontSize(18)
.margin(8)
Column() {
Text('脂肪')
.fontSize(16)
Text(${this.foodInfo.ingredients.reduce((sum, ing) => sum + ing.fat, 0).toFixed(1)}g)
.fontSize(18)
.margin(8)
Column() {
Text('碳水')
.fontSize(16)
Text(${this.foodInfo.ingredients.reduce((sum, ing) => sum + ing.carbs, 0).toFixed(1)}g)
.fontSize(18)
.margin(8)
.justifyContent(FlexAlign.SpaceAround)
.margin({ top: 12 })
// 食材列表
Text('食材组成')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 24 })
ForEach(this.foodInfo.ingredients, (ingredient) => {
Column() {
Row() {
Text(ingredient.name)
.fontSize(16)
.layoutWeight(1)
Text({ingredient.amount}{ingredient.unit})
.fontSize(16)
.fontColor('#666666')
Text(${ingredient.calories.toFixed(0)}大卡)
.fontSize(16)
.fontColor('#FF5722')
.margin({ top: 8 })
Divider()
})
// 健康建议
if (this.healthAdvice) {
Text('健康建议')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 24 })
Text(this.healthAdvice.suggestion)
.fontSize(16)
.margin({ top: 8 })
.fontColor(this.healthAdvice.remainingCalories > 0 ? '#4CAF50' : '#FF5722')
ForEach(this.healthAdvice.balancedDiet, (advice, index) => {
Text(• ${advice})
.fontSize(16)
.margin({ top: index === 0 ? 12 : 4 })
})
}
.padding(16)
}
}
private async loadHealthAdvice(): Promise<void> {
if (this.foodInfo) {
const userProfile = await foodSyncService.getUserProfile();
if (userProfile) {
const date = getCurrentDate();
const records = await foodSyncService.getMealRecords(date);
const dailyIntake = calorieService.calculateDailyIntake(records);
this.healthAdvice = calorieService.getHealthAdvice(dailyIntake, userProfile);
}
}
饮食记录界面
// MealHistoryView.ets
@Component
struct MealHistoryView {
@State records: MealRecord[] = [];
@State selectedDate: string = getCurrentDate();
@State dailyIntake: DailyIntake = { calories: 0, protein: 0, fat: 0, carbs: 0 };
aboutToAppear() {
this.loadRecords();
EventBus.on(‘mealRecordUpdated’, () => this.loadRecords());
build() {
Column() {
// 日期选择
Row() {
Button('<')
.onClick(() => this.changeDate(-1))
Text(this.selectedDate)
.fontSize(18)
.margin({ left: 16, right: 16 })
Button('>')
.onClick(() => this.changeDate(1))
.enabled(this.selectedDate !== getCurrentDate())
.margin({ top: 16 })
.justifyContent(FlexAlign.Center)
// 每日总摄入
Text(总摄入: ${this.dailyIntake.calories.toFixed(0)} 大卡)
.fontSize(20)
.margin({ top: 16 })
// 营养比例图表
Row() {
Stack() {
// 蛋白质
Column() {
Text('')
.width('100%')
.height(${Math.min(100, (this.dailyIntake.protein 4 / this.dailyIntake.calories) 100)}%)
.backgroundColor('#4CAF50')
.width(‘30%’)
.height('100%')
// 脂肪
Column() {
Text('')
.width('100%')
.height(${Math.min(100, (this.dailyIntake.fat 9 / this.dailyIntake.calories) 100)}%)
.backgroundColor('#FFC107')
.width(‘30%’)
.height('100%')
.margin({ left: '35%' })
// 碳水
Column() {
Text('')
.width('100%')
.height(${Math.min(100, (this.dailyIntake.carbs 4 / this.dailyIntake.calories) 100)}%)
.backgroundColor('#2196F3')
.width(‘30%’)
.height('100%')
.margin({ left: '70%' })
.height(120)
.width('90%')
.margin({ top: 8 })
.justifyContent(FlexAlign.Center)
// 记录列表
if (this.records.length === 0) {
Text('暂无饮食记录')
.fontSize(16)
.margin({ top: 32 })
else {
List({ space: 10 }) {
ForEach(this.records, (record) => {
ListItem() {
MealRecordItem({ record })
})
.layoutWeight(1)
}
private async loadRecords(): Promise<void> {
this.records = await foodSyncService.getMealRecords(this.selectedDate);
this.records.sort((a, b) => b.time.localeCompare(a.time));
this.dailyIntake = calorieService.calculateDailyIntake(this.records);
private async changeDate(days: number): Promise<void> {
const date = new Date(this.selectedDate);
date.setDate(date.getDate() + days);
this.selectedDate = {date.getFullYear()}-{date.getMonth() + 1}-${date.getDate()};
await this.loadRecords();
}
@Component
struct MealRecordItem {
private record: MealRecord;
build() {
Row() {
Column() {
Text(this.record.foodInfo.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text(this.record.time)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 4 })
.layoutWeight(1)
Text(${this.record.totalCalories.toFixed(0)}大卡)
.fontSize(18)
.fontColor('#FF5722')
.padding(12)
.onClick(() => {
EventBus.emit('showFoodDetail', this.record.foodInfo);
})
}
四、高级功能实现
多设备协同记录
// CollaborativeMealRecording.ets
class CollaborativeMealRecording {
private static instance: CollaborativeMealRecording;
private constructor() {}
public static getInstance(): CollaborativeMealRecording {
if (!CollaborativeMealRecording.instance) {
CollaborativeMealRecording.instance = new CollaborativeMealRecording();
return CollaborativeMealRecording.instance;
public async startFamilySharing(familyId: string): Promise<void> {
const devices = await deviceManager.getTrustedDevices();
await Promise.all(devices.map(device =>
this.inviteDeviceToFamily(device.id, familyId)
));
private async inviteDeviceToFamily(deviceId: string, familyId: string): Promise<void> {
const ability = await featureAbility.startAbility({
bundleName: 'com.example.foodCalorie',
abilityName: 'FamilyMealAbility',
deviceId
});
await ability.call({
method: 'joinFamily',
parameters: [familyId]
});
public async shareMealRecord(record: MealRecord, familyId: string): Promise<void> {
await foodSyncService.syncMealRecord(record);
const devices = await deviceManager.getTrustedDevices();
await Promise.all(devices.map(device =>
this.sendRecordToDevice(device.id, record, familyId)
));
private async sendRecordToDevice(deviceId: string, record: MealRecord, familyId: string): Promise<void> {
const ability = await featureAbility.startAbility({
bundleName: 'com.example.foodCalorie',
abilityName: 'FamilyMealAbility',
deviceId
});
await ability.call({
method: 'receiveMealRecord',
parameters: [record, familyId]
});
}
export const collaborativeRecording = CollaborativeMealRecording.getInstance();
用户健康档案
// UserProfileService.ets
import preferences from ‘@ohos.data.preferences’;
class UserProfileService {
private static instance: UserProfileService;
private constructor() {}
public static getInstance(): UserProfileService {
if (!UserProfileService.instance) {
UserProfileService.instance = new UserProfileService();
return UserProfileService.instance;
public async saveUserProfile(profile: UserProfile): Promise<void> {
await foodSyncService.syncUserProfile(profile);
// 本地也保存一份
const prefs = await preferences.getPreferences('user_profile');
await prefs.put('profile', JSON.stringify(profile));
await prefs.flush();
public async getUserProfile(): Promise<UserProfile | null> {
// 先从分布式数据获取
const distributedProfile = await foodSyncService.getUserProfile();
if (distributedProfile) {
return distributedProfile;
// 如果分布式数据没有,从本地获取
const prefs = await preferences.getPreferences('user_profile');
const localProfile = await prefs.get('profile', '');
return localProfile ? JSON.parse(localProfile) : null;
public async calculateBMI(weight: number, height: number): Promise<number> {
return weight / Math.pow(height / 100, 2);
public async getBMIStatus(bmi: number): Promise<string> {
if (bmi < 18.5) return '偏瘦';
if (bmi < 24) return '正常';
if (bmi < 28) return '超重';
return '肥胖';
}
export const userProfileService = UserProfileService.getInstance();
饮食计划提醒
// MealReminderService.ets
import reminderAgent from ‘@ohos.reminderAgent’;
class MealReminderService {
private static instance: MealReminderService;
private constructor() {}
public static getInstance(): MealReminderService {
if (!MealReminderService.instance) {
MealReminderService.instance = new MealReminderService();
return MealReminderService.instance;
public async scheduleMealReminder(mealType: string, time: string): Promise<void> {
const [hours, minutes] = time.split(':').map(Number);
const reminderTime = new Date();
reminderTime.setHours(hours, minutes, 0, 0);
const reminderRequest: reminderAgent.ReminderRequest = {
reminderType: reminderAgent.ReminderType.REMINDER_TYPE_TIMER,
actionButton: [{ title: '已用餐' }, { title: '稍后提醒' }],
wantAgent: {
pkgName: 'com.example.foodCalorie',
abilityName: 'MealReminderAbility'
},
ringDuration: 60,
snoozeTimes: 2,
triggerTime: reminderTime.getTime(),
repeatInterval: 24 60 60 * 1000, // 每天重复
title: ${mealType}时间到,
content: 该吃${mealType}了,记得记录哦!,
expiredContent: "用餐提醒已过期"
};
await reminderAgent.publishReminder(reminderRequest);
public async cancelAllMealReminders(): Promise<void> {
const reminders = await reminderAgent.getValidReminders();
await Promise.all(reminders.map(rem =>
reminderAgent.cancelReminder(rem.id)
));
}
export const mealReminderService = MealReminderService.getInstance();
五、总结
本简易菜品热量计算应用实现了以下核心价值:
精准识别:利用AI技术准确识别常见菜品和分量
热量计算:精确计算菜品总热量和营养成分
健康管理:根据用户目标提供个性化饮食建议
多设备协同:支持家庭成员共享饮食记录
习惯养成:通过提醒帮助用户规律饮食
扩展方向:
增加菜品DIY功能,自定义食材和分量
开发饮食计划生成功能
集成运动数据,计算热量平衡
增加社区分享功能,分享健康食谱
注意事项:
需要申请ohos.permission.CAMERA权限
菜品识别准确率受图片质量和菜品特征可见度影响
热量计算为估算值,实际可能有偏差
多设备协同需保持网络连接
健康建议仅供参考,具体饮食请咨询专业营养师
