Flutter & OpenHarmony 运动App体重管理组件开发

前言
体重管理是运动健康应用中帮助用户追踪身体变化的核心功能。通过记录和分析体重数据,用户可以了解运动和饮食对身体的影响,制定科学的减脂或增肌计划。本文将详细介绍如何在Flutter与OpenHarmony平台上实现完善的体重管理组件,包括体重记录、BMI计算、趋势分析、目标设定等功能模块的完整实现方案。
体重管理功能的设计需要考虑数据的准确性和用户的心理感受。体重波动是正常的,我们需要帮助用户理解短期波动和长期趋势的区别,避免因为一两天的体重变化而产生焦虑。同时,提供科学的健康指标和建议,帮助用户建立正确的体重管理观念。
Flutter体重记录模型
class WeightRecord {
final String id;
final double weight;
final DateTime date;
final String? note;
final double? bodyFat;
final double? muscleMass;
WeightRecord({
required this.id,
required this.weight,
required this.date,
this.note,
this.bodyFat,
this.muscleMass,
});
Map<String, dynamic> toJson() => {
'id': id,
'weight': weight,
'date': date.toIso8601String(),
'note': note,
'bodyFat': bodyFat,
'muscleMass': muscleMass,
};
factory WeightRecord.fromJson(Map<String, dynamic> json) {
return WeightRecord(
id: json['id'],
weight: json['weight'],
date: DateTime.parse(json['date']),
note: json['note'],
bodyFat: json['bodyFat'],
muscleMass: json['muscleMass'],
);
}
}
体重记录模型定义了体重数据的完整结构。除了核心的体重值和日期外,还支持记录备注、体脂率和肌肉量等扩展数据。备注字段让用户可以记录特殊情况,如饮食变化、生理期等影响体重的因素。体脂率和肌肉量是更专业的身体成分指标,配合智能体脂秤使用。toJson和fromJson方法支持数据的序列化和反序列化,便于存储和传输。这种设计既满足基础需求,又为高级用户提供更详细的数据记录能力。
OpenHarmony体重数据存储
import relationalStore from '@ohos.data.relationalStore';
class WeightStorageService {
private rdbStore: relationalStore.RdbStore | null = null;
async initDatabase(context: Context): Promise<void> {
const config: relationalStore.StoreConfig = {
name: 'weight.db',
securityLevel: relationalStore.SecurityLevel.S1,
};
this.rdbStore = await relationalStore.getRdbStore(context, config);
await this.rdbStore.executeSql(
'CREATE TABLE IF NOT EXISTS weight_records (id TEXT PRIMARY KEY, weight REAL, date TEXT, note TEXT, body_fat REAL, muscle_mass REAL)'
);
}
async addRecord(record: object): Promise<void> {
if (this.rdbStore) {
let valueBucket = {
'id': record['id'],
'weight': record['weight'],
'date': record['date'],
'note': record['note'] || null,
'body_fat': record['bodyFat'] || null,
'muscle_mass': record['muscleMass'] || null,
};
await this.rdbStore.insert('weight_records', valueBucket);
}
}
async getRecordsByDateRange(startDate: string, endDate: string): Promise<Array<object>> {
let records: Array<object> = [];
if (this.rdbStore) {
let predicates = new relationalStore.RdbPredicates('weight_records');
predicates.between('date', startDate, endDate).orderByAsc('date');
let resultSet = await this.rdbStore.query(predicates);
while (resultSet.goToNextRow()) {
records.push({
id: resultSet.getString(resultSet.getColumnIndex('id')),
weight: resultSet.getDouble(resultSet.getColumnIndex('weight')),
date: resultSet.getString(resultSet.getColumnIndex('date')),
});
}
resultSet.close();
}
return records;
}
}
体重数据存储服务使用关系型数据库管理体重记录。表结构包含id、体重、日期、备注、体脂率和肌肉量字段,支持完整的体重数据存储。addRecord方法插入新记录,可选字段使用null处理。getRecordsByDateRange方法按日期范围查询记录,结果按日期升序排列,便于绑制趋势图表。使用关系型数据库的优势是支持复杂查询,如按时间范围筛选、统计平均值等,满足体重分析的各种需求。
Flutter BMI计算组件
class BMICalculator {
static double calculateBMI(double weightKg, double heightCm) {
double heightM = heightCm / 100;
return weightKg / (heightM * heightM);
}
static String getBMICategory(double bmi) {
if (bmi < 18.5) return '偏瘦';
if (bmi < 24) return '正常';
if (bmi < 28) return '偏胖';
return '肥胖';
}
static Color getBMICategoryColor(double bmi) {
if (bmi < 18.5) return Colors.blue;
if (bmi < 24) return Colors.green;
if (bmi < 28) return Colors.orange;
return Colors.red;
}
static String getHealthAdvice(double bmi) {
if (bmi < 18.5) {
return '您的体重偏轻,建议适当增加营养摄入,配合力量训练增加肌肉量。';
} else if (bmi < 24) {
return '您的体重在健康范围内,继续保持良好的运动和饮食习惯。';
} else if (bmi < 28) {
return '您的体重略微超标,建议增加有氧运动,控制饮食热量摄入。';
} else {
return '您的体重超标较多,建议咨询专业人士制定减重计划。';
}
}
}
BMI计算组件提供身体质量指数的计算和解读。calculateBMI方法使用标准公式计算BMI值,体重除以身高的平方。getBMICategory方法根据中国成人BMI标准划分体重类别,18.5以下为偏瘦,18.5-24为正常,24-28为偏胖,28以上为肥胖。getBMICategoryColor方法为不同类别分配颜色,绿色表示健康,红色表示需要关注。getHealthAdvice方法根据BMI提供个性化的健康建议,帮助用户了解自己的身体状况和改进方向。
Flutter体重输入组件
class WeightInputWidget extends StatefulWidget {
final double? initialWeight;
final Function(double) onWeightChanged;
const WeightInputWidget({
Key? key,
this.initialWeight,
required this.onWeightChanged,
}) : super(key: key);
State<WeightInputWidget> createState() => _WeightInputWidgetState();
}
class _WeightInputWidgetState extends State<WeightInputWidget> {
late double _weight;
void initState() {
super.initState();
_weight = widget.initialWeight ?? 60.0;
}
Widget build(BuildContext context) {
return Column(
children: [
Text('${_weight.toStringAsFixed(1)} kg', style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove_circle_outline, size: 36),
onPressed: () => _adjustWeight(-0.1),
),
SizedBox(width: 16),
Container(
width: 200,
child: Slider(
value: _weight,
min: 30,
max: 150,
divisions: 1200,
onChanged: (value) {
setState(() => _weight = value);
widget.onWeightChanged(value);
},
),
),
SizedBox(width: 16),
IconButton(
icon: Icon(Icons.add_circle_outline, size: 36),
onPressed: () => _adjustWeight(0.1),
),
],
),
],
);
}
void _adjustWeight(double delta) {
setState(() {
_weight = (_weight + delta).clamp(30.0, 150.0);
});
widget.onWeightChanged(_weight);
}
}
体重输入组件提供直观的体重录入界面。中央大字体显示当前体重值,精确到0.1公斤。Slider组件支持滑动快速调整,范围设置为30-150公斤,divisions设为1200实现0.1公斤的精度。两侧的加减按钮支持微调,每次调整0.1公斤,适合精确输入。clamp方法确保体重值不会超出合理范围。这种组合输入方式既支持快速粗调,又支持精确微调,满足不同用户的输入习惯。
OpenHarmony体重同步服务
import http from '@ohos.net.http';
class WeightSyncService {
async syncToCloud(records: Array<object>): Promise<boolean> {
let httpRequest = http.createHttp();
try {
let response = await httpRequest.request(
'https://api.fitness.com/weight/sync',
{
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: JSON.stringify({ records: records }),
}
);
return response.responseCode === 200;
} catch (error) {
console.error('同步失败: ' + error);
return false;
} finally {
httpRequest.destroy();
}
}
async fetchFromCloud(userId: string): Promise<Array<object>> {
let httpRequest = http.createHttp();
try {
let response = await httpRequest.request(
`https://api.fitness.com/weight/records?userId=${userId}`,
{ method: http.RequestMethod.GET }
);
if (response.responseCode === 200) {
return JSON.parse(response.result as string);
}
return [];
} catch (error) {
console.error('获取数据失败: ' + error);
return [];
} finally {
httpRequest.destroy();
}
}
}
体重同步服务实现数据的云端备份和多设备同步。syncToCloud方法将本地记录上传到服务器,使用POST请求发送JSON格式的数据。fetchFromCloud方法从服务器获取用户的体重记录,用于新设备登录后恢复数据。两个方法都使用try-catch处理网络异常,finally块确保HTTP请求资源被释放。云同步功能确保用户的体重数据不会因为更换设备而丢失,也支持在多个设备上查看同一份数据。
Flutter体重趋势图表
class WeightTrendChart extends StatelessWidget {
final List<WeightRecord> records;
final double? targetWeight;
const WeightTrendChart({
Key? key,
required this.records,
this.targetWeight,
}) : super(key: key);
Widget build(BuildContext context) {
if (records.isEmpty) {
return Center(child: Text('暂无数据'));
}
return Container(
height: 200,
padding: EdgeInsets.all(16),
child: CustomPaint(
size: Size(double.infinity, 168),
painter: WeightChartPainter(
records: records,
targetWeight: targetWeight,
),
),
);
}
}
class WeightChartPainter extends CustomPainter {
final List<WeightRecord> records;
final double? targetWeight;
WeightChartPainter({required this.records, this.targetWeight});
void paint(Canvas canvas, Size size) {
double minWeight = records.map((r) => r.weight).reduce((a, b) => a < b ? a : b) - 2;
double maxWeight = records.map((r) => r.weight).reduce((a, b) => a > b ? a : b) + 2;
Paint linePaint = Paint()
..color = Colors.blue
..strokeWidth = 2
..style = PaintingStyle.stroke;
Path path = Path();
double xStep = size.width / (records.length - 1);
for (int i = 0; i < records.length; i++) {
double x = i * xStep;
double y = size.height - ((records[i].weight - minWeight) / (maxWeight - minWeight)) * size.height;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, linePaint);
if (targetWeight != null) {
double targetY = size.height - ((targetWeight! - minWeight) / (maxWeight - minWeight)) * size.height;
Paint targetPaint = Paint()
..color = Colors.green
..strokeWidth = 1
..style = PaintingStyle.stroke;
canvas.drawLine(Offset(0, targetY), Offset(size.width, targetY), targetPaint);
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
体重趋势图表以折线图形式展示体重变化。我们计算数据的最小和最大值,上下各留2公斤的边距,确保曲线不会贴边。蓝色折线连接所有数据点,展示体重的变化趋势。如果设置了目标体重,会绘制一条绿色的水平参考线,让用户直观地看到当前体重与目标的差距。这种图表帮助用户关注长期趋势而非短期波动,建立正确的体重管理心态。
Flutter体重统计卡片
class WeightStatsCard extends StatelessWidget {
final List<WeightRecord> records;
final double height;
const WeightStatsCard({
Key? key,
required this.records,
required this.height,
}) : super(key: key);
Widget build(BuildContext context) {
if (records.isEmpty) return SizedBox.shrink();
double currentWeight = records.last.weight;
double startWeight = records.first.weight;
double change = currentWeight - startWeight;
double bmi = BMICalculator.calculateBMI(currentWeight, height);
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('当前体重', '${currentWeight.toStringAsFixed(1)} kg'),
_buildStatItem('BMI', bmi.toStringAsFixed(1), color: BMICalculator.getBMICategoryColor(bmi)),
_buildStatItem('变化', '${change >= 0 ? '+' : ''}${change.toStringAsFixed(1)} kg',
color: change < 0 ? Colors.green : Colors.red),
],
),
SizedBox(height: 12),
Text(BMICalculator.getBMICategory(bmi), style: TextStyle(color: BMICalculator.getBMICategoryColor(bmi), fontWeight: FontWeight.bold)),
],
),
),
);
}
Widget _buildStatItem(String label, String value, {Color? color}) {
return Column(
children: [
Text(value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: color)),
Text(label, style: TextStyle(color: Colors.grey, fontSize: 12)),
],
);
}
}
体重统计卡片汇总展示关键的体重指标。当前体重取最新一条记录,变化值计算首条和末条记录的差值,BMI根据当前体重和身高计算。变化值使用颜色区分,减重显示绿色(积极),增重显示红色(需关注)。BMI值和类别使用对应的颜色标识健康状态。这种卡片设计信息密度高,用户一眼就能了解自己的体重状况和变化趋势,是体重管理页面的核心展示组件。
OpenHarmony体重目标服务
import dataPreferences from '@ohos.data.preferences';
class WeightGoalService {
private preferences: dataPreferences.Preferences | null = null;
async initialize(context: Context): Promise<void> {
this.preferences = await dataPreferences.getPreferences(context, 'weight_goal');
}
async setGoal(targetWeight: number, targetDate: string): Promise<void> {
if (this.preferences) {
await this.preferences.put('targetWeight', targetWeight);
await this.preferences.put('targetDate', targetDate);
await this.preferences.put('startWeight', await this.preferences.get('currentWeight', targetWeight));
await this.preferences.put('startDate', new Date().toISOString().split('T')[0]);
await this.preferences.flush();
}
}
async getGoal(): Promise<object | null> {
if (!this.preferences) return null;
let targetWeight = await this.preferences.get('targetWeight', 0) as number;
if (targetWeight === 0) return null;
return {
targetWeight: targetWeight,
targetDate: await this.preferences.get('targetDate', ''),
startWeight: await this.preferences.get('startWeight', 0),
startDate: await this.preferences.get('startDate', ''),
};
}
async calculateProgress(currentWeight: number): Promise<number> {
let goal = await this.getGoal();
if (!goal) return 0;
let totalChange = goal['startWeight'] - goal['targetWeight'];
let currentChange = goal['startWeight'] - currentWeight;
if (totalChange === 0) return 100;
return Math.min(100, Math.max(0, (currentChange / totalChange) * 100));
}
}
体重目标服务管理用户的减重或增重目标。setGoal方法设置目标体重和目标日期,同时记录开始时的体重和日期作为基准。getGoal方法获取当前目标配置,如果未设置目标则返回null。calculateProgress方法计算目标完成进度,通过比较已变化的体重和需要变化的总体重得出百分比。进度值限制在0-100之间,避免超过目标后显示异常。这种目标管理帮助用户设定明确的体重目标,并追踪达成进度。
Flutter体重记录列表
class WeightRecordList extends StatelessWidget {
final List<WeightRecord> records;
final Function(String) onDelete;
const WeightRecordList({
Key? key,
required this.records,
required this.onDelete,
}) : super(key: key);
Widget build(BuildContext context) {
return ListView.builder(
itemCount: records.length,
itemBuilder: (context, index) {
var record = records[records.length - 1 - index];
double? change;
if (index < records.length - 1) {
var previousRecord = records[records.length - 2 - index];
change = record.weight - previousRecord.weight;
}
return Dismissible(
key: Key(record.id),
direction: DismissDirection.endToStart,
onDismissed: (_) => onDelete(record.id),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 16),
child: Icon(Icons.delete, color: Colors.white),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.withOpacity(0.1),
child: Text('${record.weight.toStringAsFixed(0)}', style: TextStyle(color: Colors.blue)),
),
title: Text('${record.weight.toStringAsFixed(1)} kg'),
subtitle: Text(_formatDate(record.date)),
trailing: change != null ? Text(
'${change >= 0 ? '+' : ''}${change.toStringAsFixed(1)}',
style: TextStyle(color: change < 0 ? Colors.green : Colors.red),
) : null,
),
);
},
);
}
String _formatDate(DateTime date) {
return '${date.month}月${date.day}日';
}
}
体重记录列表展示用户的历史体重数据。列表按时间倒序排列,最新记录在最上方。每条记录显示体重值、日期和与上一条记录的变化值。变化值使用颜色区分,减重为绿色,增重为红色。Dismissible组件支持左滑删除错误记录。CircleAvatar显示体重的整数部分,提供快速的视觉参考。这种列表设计让用户可以回顾自己的体重变化历程,发现规律和问题。
总结
本文全面介绍了Flutter与OpenHarmony平台上体重管理组件的实现方案。从数据模型到存储服务,从BMI计算到趋势图表,从目标设定到记录管理,涵盖了体重管理功能的各个方面。通过科学的指标计算和直观的数据展示,我们可以帮助用户建立正确的体重管理观念,追踪身体变化,实现健康的体重目标。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)