在这里插入图片描述

说起体重管理这个功能,我自己是深有体会的。前几年因为工作压力大,经常加班熬夜,体重一路飙升,从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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐