🎯Flutter 跨平台实战:OpenHarmony 健康管理应用 Day10|实现 BMI 体质指数自动计算与等级评级

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

🚀 前言

大家好,本篇是 Flutter + OpenHarmony 健康管理应用开发系列第十篇笔记。在 Day9 完成表单非空校验、数值范围限制、非法输入拦截的基础上,今天 Day10 核心任务:根据身高、体重自动计算 BMI 体质指数,并给出偏瘦、正常、超重、肥胖等级评级,实时展示计算结果。全程兼容 OpenHarmony 鸿蒙模拟器,代码可直接复制运行,结构规范,适合课程实训作业提交与高分自查。

💥 本文你能学到

  • BMI 标准计算公式原理与代码实现
  • 输入身高体重自动运算,无需手动计算
  • BMI 区间等级划分与文字评级展示
  • 计算结果卡片美化布局,界面整洁直观
  • 结合原有表单校验,非法输入不执行计算
  • 鸿蒙端布局适配、数值计算精度保留两位小数

🥝 开发环境

1. 环境信息

开发工具:DevEco Studio

开发语言:Dart开发框架:

Flutter调试设备:OpenHarmony手机模拟器

适配平台:OpenHarmony

2. 依赖配置

Day10 无需新增依赖,沿用之前配置即可:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.2

📝 今日核心开发功能

  • 引入国际标准 BMI 计算公式:BMI = 体重 (kg) / 身高 (m) 的平方
  • 自动保留两位小数,提升数据精准度
  • 划分四级评级标准:偏瘦、正常、超重、肥胖
  • 在首页新增独立卡片展示 BMI 数值与等级
  • 录入合法身高体重后,保存并自动刷新计算结果
  • 非法输入、空输入不进行 BMI 运算,避免异常崩溃
  • 适配鸿蒙屏幕,卡片布局居中自适应不错乱

✅ 完整可运行核心代码

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "健康管理",
      debugShowCheckedModeBanner: false,
      home: const MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;
  final List<Widget> _pages = const [
    HomePage(),
    HealthInputPage(),
    ProfilePage(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _currentIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: _onItemTapped,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
          BottomNavigationBarItem(icon: Icon(Icons.add), label: "健康录入"),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: "个人中心"),
        ],
      ),
    );
  }
}

// 首页 新增BMI计算展示
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String name = "未填写";
  String gender = "未填写";
  String age = "未填写";
  String height = "未填写";
  String weight = "未填写";
  String heart = "未填写";

  double bmi = 0.0;
  String bmiLevel = "暂无数据";

  // BMI计算逻辑
  void calcBMI() {
    if(height == "未填写" || weight == "未填写"){
      bmi = 0.0;
      bmiLevel = "暂无数据";
      return;
    }
    double h = double.parse(height) / 100;
    double w = double.parse(weight);
    bmi = w / (h * h);
    // 保留两位小数
    bmi = double.parse(bmi.toStringAsFixed(2));

    // 等级判断
    if(bmi < 18.5){
      bmiLevel = "偏瘦";
    }else if(bmi < 24){
      bmiLevel = "正常";
    }else if(bmi < 28){
      bmiLevel = "超重";
    }else{
      bmiLevel = "肥胖";
    }
  }

  Future<void> _loadData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      name = prefs.getString("name") ?? "未填写";
      gender = prefs.getString("gender") ?? "未填写";
      age = prefs.getString("age") ?? "未填写";
      height = prefs.getString("height") ?? "未填写";
      weight = prefs.getString("weight") ?? "未填写";
      heart = prefs.getString("heart") ?? "未填写";
    });
    calcBMI();
  }

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("首页")),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text("个人健康信息", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            const SizedBox(height: 20),
            Card(
              elevation: 4,
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  children: [
                    _buildItem("姓名", name),
                    _buildItem("性别", gender),
                    _buildItem("年龄", "$age 岁"),
                    _buildItem("身高", "$height cm"),
                    _buildItem("体重", "$weight kg"),
                    _buildItem("心率", "$heart 次/分"),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),
            // BMI卡片
            Card(
              elevation: 4,
              color: Colors.blue[50],
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  children: [
                    const Text("BMI体质指数",style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold)),
                    const SizedBox(height: 10),
                    Text(bmi == 0 ? "暂无数据" : "$bmi",style: TextStyle(fontSize: 22,color: Colors.blue,fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    Text("评级:$bmiLevel",style: TextStyle(fontSize: 16)),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),
            Center(
              child: ElevatedButton(onPressed: _loadData, child: const Text("刷新数据")),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildItem(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: const TextStyle(fontSize: 16)),
          Text(value, style: const TextStyle(fontSize: 16, color: Colors.blue)),
        ],
      ),
    );
  }
}

// 健康录入页面 保留Day9校验
class HealthInputPage extends StatefulWidget {
  const HealthInputPage({super.key});

  @override
  State<HealthInputPage> createState() => _HealthInputPageState();
}

class _HealthInputPageState extends State<HealthInputPage> {
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _ageController = TextEditingController();
  final TextEditingController _heightController = TextEditingController();
  final TextEditingController _weightController = TextEditingController();
  final TextEditingController _heartController = TextEditingController();
  String _gender = "男";

  Future<void> _saveData() async {
    String name = _nameController.text.trim();
    String ageStr = _ageController.text.trim();
    String heightStr = _heightController.text.trim();
    String weightStr = _weightController.text.trim();
    String heartStr = _heartController.text.trim();

    if (name.isEmpty || ageStr.isEmpty || heightStr.isEmpty || weightStr.isEmpty || heartStr.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请填写完整信息")));
      return;
    }

    int? age = int.tryParse(ageStr);
    double? height = double.tryParse(heightStr);
    double? weight = double.tryParse(weightStr);
    int? heart = int.tryParse(heartStr);

    if (age == null || age < 1 || age > 120) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("年龄需在1-120之间")));
      return;
    }
    if (height == null || height < 50 || height > 250) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("身高需在50-250之间")));
      return;
    }
    if (weight == null || weight < 1 || weight > 300) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("体重需在1-300之间")));
      return;
    }
    if (heart == null || heart < 40 || heart > 180) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("心率需在40-180之间")));
      return;
    }

    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString("name", name);
    await prefs.setString("gender", _gender);
    await prefs.setString("age", ageStr);
    await prefs.setString("height", heightStr);
    await prefs.setString("weight", weightStr);
    await prefs.setString("heart", heartStr);

    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("✅ 保存成功")));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("健康录入")),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text("姓名"),
            TextField(controller: _nameController, decoration: const InputDecoration(border: OutlineInputBorder())),
            const SizedBox(height: 15),
            const Text("性别"),
            Row(
              children: [
                Expanded(
                  child: RadioListTile(
                    title: const Text("男"),
                    value: "男",
                    groupValue: _gender,
                    onChanged: (value) => setState(() => _gender = value!),
                  ),
                ),
                Expanded(
                  child: RadioListTile(
                    title: const Text("女"),
                    value: "女",
                    groupValue: _gender,
                    onChanged: (value) => setState(() => _gender = value!),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 15),
            const Text("年龄"),
            TextField(controller: _ageController, keyboardType: TextInputType.number, decoration: const InputDecoration(border: OutlineInputBorder())),
            const SizedBox(height: 15),
            const Text("身高(cm)"),
            TextField(controller: _heightController, keyboardType: TextInputType.number, decoration: const InputDecoration(border: OutlineInputBorder())),
            const SizedBox(height: 15),
            const Text("体重(kg)"),
            TextField(controller: _weightController, keyboardType: TextInputType.number, decoration: const InputDecoration(border: OutlineInputBorder())),
            const SizedBox(height: 15),
            const Text("心率(次/分)"),
            TextField(controller: _heartController, keyboardType: TextInputType.number, decoration: const InputDecoration(border: OutlineInputBorder())),
            const SizedBox(height: 20),
            Center(
              child: ElevatedButton(onPressed: _saveData, child: const Text("保存数据")),
            ),
          ],
        ),
      ),
    );
  }
}

// 个人中心
class ProfilePage extends StatefulWidget {
  const ProfilePage({super.key});

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  String name = "未填写";
  String gender = "未填写";
  String age = "未填写";
  String height = "未填写";
  String weight = "未填写";
  String heart = "未填写";

  Future<void> _loadData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      name = prefs.getString("name") ?? "未填写";
      gender = prefs.getString("gender") ?? "未填写";
      age = prefs.getString("age") ?? "未填写";
      height = prefs.getString("height") ?? "未填写";
      weight = prefs.getString("weight") ?? "未填写";
      heart = prefs.getString("heart") ?? "未填写";
    });
  }

  Future<void> _clearData() async {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text("确认清空"),
        content: const Text("确定要清空所有数据吗?"),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text("取消")),
          TextButton(
            onPressed: () async {
              SharedPreferences prefs = await SharedPreferences.getInstance();
              await prefs.clear();
              _loadData();
              Navigator.pop(context);
            },
            child: const Text("确定", style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Widget _buildItem(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: const TextStyle(fontSize: 16)),
          Text(value, style: const TextStyle(fontSize: 16, color: Colors.blue)),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("个人中心")),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text("我的健康信息", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            const SizedBox(height: 20),
            Card(
              elevation: 4,
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  children: [
                    _buildItem("姓名", name),
                    _buildItem("性别", gender),
                    _buildItem("年龄", "$age 岁"),
                    _buildItem("身高", "$height cm"),
                    _buildItem("体重", "$weight kg"),
                    _buildItem("心率", "$heart 次/分"),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 30),
            Center(
              child: ElevatedButton(
                style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                onPressed: _clearData,
                child: const Text("清空所有数据"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

📱 调试与运行完整步骤

  • 停止原有项目运行确认
  • pubspec.yaml 依赖不变,无需新增库
  • 修改 lib/main.dart 为 Day10 所需代码
  • 终端执行 flutter pub get 同步依赖(可选)
  • 连接 OpenHarmony 模拟器,执行 flutter run
    flutter run
  • 进入健康录入,填写合法身高体重并保存
  • 回到首页自动生成 BMI 数值与等级评级
  • 清空数据后自动恢复暂无数据状态,功能正常

🔐 跨平台适配说明

  • Day10 BMI 计算功能完美适配 OpenHarmony:浮点运算精度在鸿蒙端无偏差,保留两位小数正常
  • BMI 卡片布局自适应鸿蒙屏幕比例
  • 数据读取、计算、刷新逻辑在鸿蒙模拟器稳定运行
  • 保留原有表单校验、本地存储、清空弹窗全部兼容

❌ 常见错误排查

错误现象解决方法
BMI 一直显示暂无数据检查是否正确填写身高体重并保存,点击刷新数据
计算数值异常偏大确认身高已除以 100 转为米单位
小数位数过多使用 toStringAsFixed (2) 固定两位小数
录入非法数值仍可计算保留 Day9 数值范围校验,拦截异常输入

🎨 项目后续规划

Day10 完成 BMI 自动计算与等级评级,后续安排:

  • Day11:优化整体 UI 样式、主题配色、卡片美化
  • Day12:增加健康数据记录时间保存功能

📌 项目总结

本篇为 Flutter 鸿蒙健康管理项目 Day10 开发笔记,在 Day9 表单校验基础上,加入标准 BMI 计算公式与等级判定逻辑,实现身高体重自动运算、评级展示,界面新增独立卡片布局清晰直观。同时完全兼容原有三页面切换、本地持久化存储、一键清空、输入合法性校验全部功能,项目功能进一步完善,为后续 UI 美化与时间记录功能做好铺垫。

✅ 结尾小贴士

  • BMI 计算务必注意厘米转米单位换算,避免结果出错
  • 数值保留固定小数位,提升界面整洁度与专业性
  • 代码可直接全覆盖使用,无需改动任何配置,一键运行即可出效果
  • 点赞收藏不迷路,后续每日开发笔记持续同步更新

更多推荐