Flutter 跨平台实战:OpenHarmony 健康管理应用 Day10|实现 BMI 体质指数自动计算与等级评级
·
🎯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 计算务必注意厘米转米单位换算,避免结果出错
- 数值保留固定小数位,提升界面整洁度与专业性
- 代码可直接全覆盖使用,无需改动任何配置,一键运行即可出效果
- 点赞收藏不迷路,后续每日开发笔记持续同步更新
更多推荐
所有评论(0)