Flutter 跨平台实战OpenHarmony 健康管理应用 Day15引入图表依赖搭建数据可视化基础欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net 前言大家好本篇是 FlutterOpenHarmony 健康管理应用开发系列第十五篇笔记。在 Day14 完成 BMI 结果页面展示与等级判断的基础上今日引入 fl_chart 第三方图表库完成依赖配置并搭建健康数据可视化基础框架为后续折线图绘制做准备代码兼容 OpenHarmony 鸿蒙系统。 本文你能学到fl_chart 图表库在 Flutter 项目中的标准引入方式双第三方库共存配置shared_preferences fl_chart鸿蒙系统下图表组件基础布局适配搭建健康数据可视化页面基础结构保留前期所有业务功能无改动 开发环境1. 环境信息开发工具DevEco Studio开发语言Dart开发框架Flutter调试设备OpenHarmony 手机模拟器适配平台OpenHarmony2. 依赖配置dependencies: flutter: sdk: flutter shared_preferences: ^2.2.2 fl_chart: ^0.65.0 今日核心开发功能新增引入fl_chart第三方图表依赖配置 pubspec.yaml 并同步依赖在首页搭建图表容器基础布局完成鸿蒙环境图表组件基础适配保留表单校验、BMI 计算、本地存储、时间记录、退出弹窗等全部原有功能✅ 完整可运行代码import package:flutter/material.dart; import package:shared_preferences/shared_preferences.dart; import package:fl_chart/fl_chart.dart; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); override Widget build(BuildContext context) { return MaterialApp( title: 健康管理, debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.teal, pageTransitionsTheme: PageTransitionsTheme( builders: { TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), }, ), cardTheme: CardTheme( elevation: 6, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), margin: EdgeInsets.symmetric(horizontal: 4), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( padding: EdgeInsets.symmetric(horizontal: 30, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), home: const MainPage(), ); } } class MainPage extends StatefulWidget { const MainPage({super.key}); override StateMainPage createState() _MainPageState(); } class _MainPageState extends StateMainPage { int _currentIndex 0; final ListWidget _pages const [ HomePage(), HealthInputPage(), ProfilePage(), AboutPage(), ]; void _onItemTapped(int index) { setState(() { _currentIndex index; }); } Futurebool _onWillPop() async { return await showDialog( context: context, builder: (context) AlertDialog( title: const Text(退出提示), content: const Text(确定要退出应用吗), actions: [ TextButton( onPressed: () Navigator.of(context).pop(false), child: const Text(取消), ), TextButton( onPressed: () Navigator.of(context).pop(true), child: const Text(确定, style: TextStyle(color: Colors.red)), ), ], ), ) ?? false; } override Widget build(BuildContext context) { return WillPopScope( onWillPop: _onWillPop, child: Scaffold( body: _pages[_currentIndex], bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: _currentIndex, onTap: _onItemTapped, selectedItemColor: Colors.teal, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 首页), BottomNavigationBarItem(icon: Icon(Icons.add_box), label: 健康录入), BottomNavigationBarItem(icon: Icon(Icons.person), label: 个人中心), BottomNavigationBarItem(icon: Icon(Icons.info), label: 关于), ], ), ), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); override StateHomePage createState() _HomePageState(); } class _HomePageState extends StateHomePage { String name 未填写; String gender 未填写; String age 未填写; String height 未填写; String weight 未填写; String heart 未填写; String saveTime 暂无记录时间; double bmi 0.0; String bmiLevel 暂无数据; 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 肥胖; } } Futurevoid _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) ?? 未填写; saveTime prefs.getString(saveTime) ?? 暂无记录时间; }); calcBMI(); } override void initState() { super.initState(); _loadData(); } Widget _buildItem(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 16)), Text(value, style: TextStyle(fontSize: 16, color: Colors.teal[600], fontWeight: FontWeight.w500)), ], ), ); } 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: 22, fontWeight: FontWeight.bold)), const SizedBox(height: 20), Card( child: Padding( padding: const EdgeInsets.all(22), child: Column( children: [ _buildItem(姓名, name), _buildItem(性别, gender), _buildItem(年龄, $age 岁), _buildItem(身高, $height cm), _buildItem(体重, $weight kg), _buildItem(心率, $heart 次/分), _buildItem(录入时间, saveTime), ], ), ), ), const SizedBox(height: 20), Card( color: Colors.teal[50], child: Padding( padding: const EdgeInsets.all(22), child: Column( children: [ const Text(BMI体质指数, style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold)), const SizedBox(height: 12), Text(bmi 0 ? 暂无数据 : $bmi, style: TextStyle(fontSize: 24, color: Colors.teal[700], fontWeight: FontWeight.bold)), const SizedBox(height: 10), Text(健康评级$bmiLevel, style: TextStyle(fontSize: 17)), ], ), ), ), const SizedBox(height: 20), // Day15 新增图表可视化基础容器 Card( child: Padding( padding: const EdgeInsets.all(22), child: Column( children: [ const Text(健康数据可视化, style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold)), const SizedBox(height: 15), SizedBox( height: 180, child: LineChart( LineChartData(), ), ), ], ), ), ), const SizedBox(height: 30), Center( child: ElevatedButton(onPressed: _loadData, child: const Text(刷新数据)), ), ], ), ), ); } } class HealthInputPage extends StatefulWidget { const HealthInputPage({super.key}); override StateHealthInputPage createState() _HealthInputPageState(); } class _HealthInputPageState extends StateHealthInputPage { final TextEditingController _nameController TextEditingController(); final TextEditingController _ageController TextEditingController(); final TextEditingController _heightController TextEditingController(); final TextEditingController _weightController TextEditingController(); final TextEditingController _heartController TextEditingController(); String _gender 男; String _getNowTime() { DateTime now DateTime.now(); return ${now.year}-${now.month}-${now.day} ${now.hour}:${now.minute}; } Futurevoid _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; } String nowTime _getNowTime(); 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); await prefs.setString(saveTime, nowTime); 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(姓名, style: TextStyle(fontSize: 16)), SizedBox(height: 8), TextField( controller: _nameController, decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 12), ), ), const SizedBox(height: 18), const Text(性别, style: TextStyle(fontSize: 16)), 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: 10), const Text(年龄, style: TextStyle(fontSize: 16)), SizedBox(height: 8), TextField( controller: _ageController, keyboardType: TextInputType.number, decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 12), ), ), const SizedBox(height: 18), const Text(身高(cm), style: TextStyle(fontSize: 16)), SizedBox(height: 8), TextField( controller: _heightController, keyboardType: TextInputType.number, decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 12), ), ), const SizedBox(height: 18), const Text(体重(kg), style: TextStyle(fontSize: 16)), SizedBox(height: 8), TextField( controller: _weightController, keyboardType: TextInputType.number, decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 12), ), ), const SizedBox(height: 18), const Text(心率(次/分), style: TextStyle(fontSize: 16)), SizedBox(height: 8), TextField( controller: _heartController, keyboardType: TextInputType.number, decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 12), ), ), const SizedBox(height: 30), Center( child: ElevatedButton(onPressed: _saveData, child: const Text(保存数据)), ), ], ), ), ); } } class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); override StateProfilePage createState() _ProfilePageState(); } class _ProfilePageState extends widgetProfilePage { String name 未填写; String gender 未填写; String age 未填写; String height 未填写; String weight 未填写; String heart 未填写; String saveTime 暂无记录时间; Futurevoid _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) ?? 未填写; saveTime prefs.getString(saveTime) ?? 暂无记录时间; }); } Futurevoid _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: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 16)), Text(value, style: TextStyle(fontSize: 16, color: Colors.teal[600], fontWeight: FontWeight.w500)), ], ), ); } 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: 22, fontWeight: FontWeight.bold)), const SizedBox(height: 20), Card( child: Padding( padding: const EdgeInsets.all(22), child: Column( children: [ _buildItem(姓名, name), _buildItem(性别, gender), _buildItem(年龄, $age 岁), _buildItem(身高, $height cm), _buildItem(体重, $weight kg), _buildItem(心率, $heart 次/分), _buildItem(录入时间, saveTime), ], ), ), ), const SizedBox(height: 30), Center( child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), onPressed: _clearData, child: const Text(清空所有数据), ), ), ], ), ), ); } } class AboutPage extends StatelessWidget { const AboutPage({super.key}); override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(关于我们)), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( children: [ const SizedBox(height: 40), const Icon(Icons.health_and_safety, size: 80, color: Colors.teal), const SizedBox(height: 20), const Text( 健康管理App, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), const Text( 版本号V1.5, style: TextStyle(fontSize: 16, color: Colors.grey), ), const SizedBox(height: 30), Card( child: Padding( padding: const EdgeInsets.all(22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text(应用介绍, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), SizedBox(height: 12), Text( 本应用基于Flutter开发适配OpenHarmony鸿蒙系统。支持个人健康信息录入、表单合法校验、BMI体质指数自动计算、本地数据持久化存储、录入时间记录、全局UI美化、页面跳转动画、返回键退出弹窗等完整功能。, style: TextStyle(fontSize: 15, height: 1.6), ), SizedBox(height: 20), Text(技术栈, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), SizedBox(height: 12), Text( Flutter Dart SharedPreferences fl_chart, style: TextStyle(fontSize: 15), ), SizedBox(height: 20), Text(开发用途, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), SizedBox(height: 12), Text( 课程实训综合项目完整覆盖页面布局、表单校验、数据存储、业务逻辑、UI美化、交互优化、数据可视化基础搭建等核心开发知识点。, style: TextStyle(fontSize: 15, height: 1.6), ), ], ), ), ), ], ), ), ); } } 调试与运行完整步骤停止项目原有运行确认 pubspec.yaml 已添加 fl_chart 依赖修改 lib/main.dart 为 Day15 完整代码执行 flutter pub get 同步依赖连接鸿蒙模拟器执行 flutter run首页查看新增图表容器组件无报错正常渲染原有数据保存、BMI 计算、时间记录、退出弹窗、页面切换功能全部正常 跨平台适配说明本次引入 fl_chart 图表库并搭建可视化基础完全适配 OpenHarmony 系统图表组件在鸿蒙端正常渲染无兼容异常原有全部业务逻辑与 UI 样式保持稳定。❌ 常见错误排查错误现象解决方法fl_chart 导入报错检查 yaml 依赖是否添加并执行 pub get图表组件黑屏 / 不显示确认组件嵌套结构、高度约束配置正确鸿蒙端图表异常保持基础 LineChartData 空对象即可正常渲染 项目后续规划Day15 完成图表依赖引入与可视化基础搭建Day16 将实现健康数据折线图完整绘制展示身高、体重、心率变化趋势。 项目总结本篇 Day15 严格按照开发路线完成图表依赖引入与可视化基础框架搭建在保留前期全部功能的前提下新增 fl_chart 第三方库支持完成鸿蒙环境图表组件基础适配为后续数据可视化开发奠定基础整体项目结构更加完善。✅ 结尾小贴士fl_chart 是 Flutter 生态中成熟稳定的图表第三方库在 OpenHarmony 鸿蒙系统中兼容性良好只需正确配置依赖并设置容器高度即可正常使用适合健康类、数据类 App 快速实现可视化需求。点赞收藏不迷路后续每日开发笔记将持续同步更新