Flutter for OpenHarmony生活助手App实战:体重管理功能实现
本文介绍了生活助手App中体重管理功能的设计与实现。作者分享了个人体重管理经历,强调该功能应具备简单直观、趋势可视化、健康指标和激励机制等核心特点。文章详细解析了页面结构,包括当前体重卡片、趋势图表和BMI指数卡片三部分,重点阐述了绿色渐变卡片设计、大字体体重显示以及简化折线图表的实现思路。该功能旨在为用户提供清晰直观的体重变化趋势,帮助实现健康管理目标。

说起体重管理这个功能,我自己是深有体会的。前几年因为工作压力大,经常加班熬夜,体重一路飙升,从65公斤涨到了75公斤。后来下定决心要减肥,但总是坚持不下来,主要是因为没有一个好的记录工具。
市面上的体重管理App要么功能太复杂,要么界面太丑,用起来都不顺手。所以在做生活助手App的时候,我就想着一定要把体重管理这个功能做好,让自己和其他有同样需求的人能够方便地记录和管理体重。
为什么需要体重管理功能
体重管理不只是减肥,更重要的是健康管理。无论是想减肥、增肌,还是保持体重,都需要定期记录和监测。
我在设计这个功能的时候,有几个核心想法:
- 简单直观:打开就能看到当前体重,不要藏得太深
- 趋势可视化:用图表展示体重变化,比数字更直观
- 健康指标:不只是体重,还要有BMI等健康指标
- 激励机制:看到体重下降的曲线,会更有动力坚持
记得我自己减肥的时候,每天早上起床第一件事就是称体重,然后记录下来。看着体重一点点下降,那种成就感真的很强。这也是我为什么要做这个功能的原因。
页面整体结构
体重管理页面我分成了三个部分:当前体重卡片、体重趋势图表、BMI指数卡片。这种布局很清晰,用户一眼就能看到所有重要信息。
先看看页面的基本框架:
class WeightTrackerPage extends StatelessWidget {
const WeightTrackerPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('体重管理'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () {},
),
],
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildCurrentWeight(),
SizedBox(height: 24.h),
_buildWeightChart(),
SizedBox(height: 24.h),
_buildBMICard(),
],
),
),
);
}
AppBar右边放了一个加号按钮,点击可以添加新的体重记录。这是最常用的操作,所以放在最显眼的位置。
SingleChildScrollView确保内容多的时候可以滚动。三个主要部分用SizedBox(height: 24.h)分隔开,24这个间距让页面看起来不拥挤,也不松散。
当前体重卡片的设计
当前体重卡片是整个页面的焦点,我用了绿色渐变背景:
Widget _buildCurrentWeight() {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.green, Colors.lightGreen],
),
borderRadius: BorderRadius.circular(16.r),
),
child: Column(
children: [
Text(
'当前体重',
style: TextStyle(color: Colors.white70, fontSize: 14.sp),
),
SizedBox(height: 8.h),
为什么用绿色?因为绿色代表健康、生命力,和体重管理的主题很搭配。渐变色从深绿到浅绿,看起来更有层次感。
borderRadius: BorderRadius.circular(16.r)的圆角让卡片看起来更柔和。我试过12、14、16、18几个数值,最后发现16是最合适的,既不会太圆也不会太方。
体重数值的展示
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'65.5',
style: TextStyle(
color: Colors.white,
fontSize: 48.sp,
fontWeight: FontWeight.bold,
),
),
Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Text(
' kg',
style: TextStyle(color: Colors.white, fontSize: 20.sp),
),
),
],
),
],
),
);
}
体重数值用48号超大字体显示,这是我特意设计的。体重是用户最关心的数据,必须一眼就能看到,而且要看得很清楚。
单位"kg"用20号字体,放在右下角,padding: EdgeInsets.only(bottom: 8.h)让它和数字底部对齐。这种大小对比形成了视觉层次,数字是主角,单位是配角。
crossAxisAlignment: CrossAxisAlignment.end让数字和单位底部对齐,看起来更协调。如果用顶部对齐或居中对齐,视觉效果就没那么好了。
体重趋势图表
趋势图表是体重管理的核心功能,能让用户直观地看到体重的变化趋势:
Widget _buildWeightChart() {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'体重趋势',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 20.h),
图表容器用白色背景,和页面的灰色背景形成对比。标题"体重趋势"用粗体显示,让用户知道这是什么内容。
折线图的实现
SizedBox(
height: 200.h,
child: LineChart(
LineChartData(
gridData: const FlGridData(show: false),
titlesData: const FlTitlesData(
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
这里用了fl_chart包的LineChart组件。我把网格线、坐标轴标题、边框都隐藏了,让图表看起来更简洁。
为什么要隐藏这些?因为体重管理不需要精确的刻度,用户只需要看到趋势就够了。是上升还是下降,这才是最重要的。太多的辅助线反而会让图表显得杂乱。
数据点的配置
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 67),
FlSpot(1, 66.5),
FlSpot(2, 66.2),
FlSpot(3, 66),
FlSpot(4, 65.8),
FlSpot(5, 65.5),
],
isCurved: true,
color: Colors.green,
barWidth: 3,
dotData: const FlDotData(show: true),
),
],
),
),
),
],
),
);
}
这里展示了6个数据点,从67公斤降到65.5公斤,是一个典型的减肥过程。实际使用中,这些数据应该从存储中读取。
isCurved: true让折线变成曲线,看起来更平滑,更美观。color: Colors.green用绿色,和当前体重卡片的颜色呼应。
barWidth: 3是线条宽度,3像素刚好合适,太细了看不清,太粗了又显得笨重。dotData: const FlDotData(show: true)显示数据点,让用户知道每个点的位置。
BMI指数卡片
BMI(身体质量指数)是评估体重是否健康的重要指标:
Widget _buildBMICard() {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'BMI指数',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
BMI卡片也用白色背景,和体重趋势图表保持一致。标题用粗体,让用户知道这是BMI指数。
BMI数值和状态
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'22.5',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: Colors.green
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
'正常',
style: TextStyle(color: Colors.green, fontSize: 14.sp),
),
),
],
),
],
),
);
}
BMI数值用32号粗体绿色文字显示,绿色表示健康。右边是一个状态标签,用浅绿色背景配绿色文字,显示"正常"。
borderRadius: BorderRadius.circular(20.r)让标签变成胶囊形状,这种形状很适合做状态标签,看起来很现代。
BMI计算逻辑
BMI的计算公式是:体重(kg) / 身高(m)²
class BMICalculator {
static double calculate(double weight, double height) {
// height单位是厘米,需要转换成米
final heightInMeters = height / 100;
return weight / (heightInMeters * heightInMeters);
}
static String getStatus(double bmi) {
if (bmi < 18.5) return '偏瘦';
if (bmi < 24) return '正常';
if (bmi < 28) return '偏胖';
return '肥胖';
}
static Color getColor(double bmi) {
if (bmi < 18.5) return Colors.blue;
if (bmi < 24) return Colors.green;
if (bmi < 28) return Colors.orange;
return Colors.red;
}
}
这个工具类提供了三个方法:计算BMI、获取状态、获取颜色。不同的BMI范围对应不同的状态和颜色,让用户一眼就能看出自己的体重是否健康。
BMI小于18.5是偏瘦,用蓝色;18.5-24是正常,用绿色;24-28是偏胖,用橙色;大于28是肥胖,用红色。这种颜色编码很直观,用户不用看文字就知道自己的状态。
数据存储实现
体重数据需要持久化存储,我用的是shared_preferences:
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
class WeightStorage {
static const String _key = 'weight_records';
static Future<void> addRecord(double weight, DateTime date) async {
final prefs = await SharedPreferences.getInstance();
final records = await getRecords();
records.add({
'weight': weight,
'date': date.toIso8601String(),
});
final jsonString = jsonEncode(records);
await prefs.setString(_key, jsonString);
}
static Future<List<Map<String, dynamic>>> getRecords() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_key);
if (jsonString == null) return [];
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((e) => {
'weight': e['weight'],
'date': DateTime.parse(e['date']),
}).toList();
}
}
addRecord方法添加新的体重记录,包含体重值和日期。getRecords方法获取所有记录,返回一个列表。
存储的时候把DateTime转换成ISO8601字符串,读取的时候再转换回来。这样就实现了数据的持久化存储。
添加体重记录
点击AppBar的加号按钮,弹出对话框让用户输入体重:
void _showAddWeightDialog(BuildContext context) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加体重记录'),
content: TextField(
controller: controller,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '体重 (kg)',
hintText: '请输入体重',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
final weight = double.tryParse(controller.text);
if (weight != null) {
await WeightStorage.addRecord(weight, DateTime.now());
Navigator.pop(context);
// 刷新页面
}
},
child: const Text('保存'),
),
],
),
);
}
用AlertDialog实现对话框,简单直接。输入框限制为数字键盘keyboardType: TextInputType.number,避免用户输入非法字符。
点击保存后,先验证输入是否有效,然后保存到存储,最后关闭对话框并刷新页面。这个流程很标准,用户体验也好。
图表数据的处理
从存储读取的数据需要转换成图表能用的格式:
List<FlSpot> _convertToChartData(List<Map<String, dynamic>> records) {
if (records.isEmpty) return [];
// 按日期排序
records.sort((a, b) =>
(a['date'] as DateTime).compareTo(b['date'] as DateTime)
);
// 只取最近7天的数据
final recentRecords = records.length > 7
? records.sublist(records.length - 7)
: records;
// 转换成FlSpot格式
return recentRecords.asMap().entries.map((entry) {
return FlSpot(
entry.key.toDouble(),
entry.value['weight'] as double,
);
}).toList();
}
这个方法做了三件事:排序、截取、转换。
首先按日期排序,确保图表从左到右是时间顺序。然后只取最近7天的数据,太多数据会让图表显得拥挤。最后转换成FlSpot格式,这是fl_chart需要的数据格式。
实际使用体验
我自己用这个体重管理功能已经有一段时间了,感觉还是挺好用的。每天早上称完体重就记录一下,看着曲线一点点下降,真的很有成就感。
有时候看到体重反弹了,就会反思是不是昨天吃多了,然后今天就会注意控制饮食。这种及时的反馈很重要,能帮助我们更好地管理体重。
不过也发现了一些可以改进的地方:
1. 目标体重设置
可以让用户设置目标体重,然后在图表上显示目标线。看到当前体重和目标体重的差距,会更有动力。
实现思路是:在图表上加一条虚线,表示目标体重。当前体重高于目标就显示红色,低于目标就显示绿色。
2. 体重变化统计
可以显示一周、一月、三月的体重变化。比如"本周减重0.5kg"、“本月减重2kg”。这种统计数据能让用户更清楚自己的进度。
3. 提醒功能
可以设置每天固定时间提醒用户称体重。养成每天称体重的习惯很重要,提醒功能能帮助用户坚持。
4. 数据导出
可以把体重数据导出成Excel或图片,方便用户分享或备份。有些人喜欢把减肥成果分享到社交媒体,这个功能就很有用。
性能优化建议
体重管理功能虽然不复杂,但也要注意性能:
1. 数据缓存
体重记录不需要每次都从存储读取,可以缓存在内存中。只有在添加新记录时才更新缓存,这样能提高响应速度。
2. 图表优化
如果体重记录很多,不要全部显示在图表上。只显示最近的数据,比如最近7天或30天,这样图表更清晰,性能也更好。
3. 懒加载
如果要加历史记录列表,可以用懒加载。不要一次性加载所有记录,而是滚动到底部时再加载更多。
4. 动画优化
图表的动画要流畅,不要用太复杂的动画。fl_chart默认的动画就很好,简洁流畅。
健康建议功能
除了记录体重,还可以根据BMI给出健康建议:
String getHealthAdvice(double bmi) {
if (bmi < 18.5) {
return '您的体重偏轻,建议适当增加营养摄入,多吃高蛋白食物。';
} else if (bmi < 24) {
return '您的体重正常,请继续保持健康的生活方式。';
} else if (bmi < 28) {
return '您的体重偏重,建议适当控制饮食,增加运动量。';
} else {
return '您的体重超标,建议咨询医生,制定科学的减重计划。';
}
}
根据不同的BMI范围,给出不同的健康建议。这些建议不是医疗建议,只是一般性的健康提示。
如果BMI超标严重,建议用户咨询医生,这很重要。体重管理App不能代替专业的医疗建议。
总结
体重管理功能看起来简单,但要做好需要考虑很多细节。最重要的是要让用户愿意坚持记录,只有坚持记录,才能看到效果。
我在开发这个功能的时候,一直在思考怎么让它更好用。后来发现,好的体重管理功能不是功能最多的,而是最能激励用户坚持的。
如果你也在开发类似的功能,建议多从用户角度思考,多试用,多改进。一个好用的体重管理功能,真的能帮助人们更好地管理健康。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)