Flutter 跨平台实战:OpenHarmony 健康管理应用 Day15|引入图表依赖,搭建数据可视化基础
大家好,本篇是 Flutter+OpenHarmony 健康管理应用开发系列第十五篇笔记。在 Day14 完成 BMI 结果页面展示与等级判断的基础上,今日引入 fl_chart 第三方图表库,完成依赖配置并搭建健康数据可视化基础框架,为后续折线图绘制做准备,代码兼容 OpenHarmony 鸿蒙系统。
·
🎯Flutter 跨平台实战:OpenHarmony 健康管理应用 Day15|引入图表依赖,搭建数据可视化基础
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🚀 前言
大家好,本篇是 Flutter+OpenHarmony 健康管理应用开发系列第十五篇笔记。在 Day14 完成 BMI 结果页面展示与等级判断的基础上,今日引入 fl_chart 第三方图表库,完成依赖配置并搭建健康数据可视化基础框架,为后续折线图绘制做准备,代码兼容 OpenHarmony 鸿蒙系统。
💥 本文你能学到
- fl_chart 图表库在 Flutter 项目中的标准引入方式
- 双第三方库共存配置(shared_preferences + fl_chart)
- 鸿蒙系统下图表组件基础布局适配
- 搭建健康数据可视化页面基础结构
- 保留前期所有业务功能无改动
🥝 开发环境
1. 环境信息
- 开发工具:DevEco Studio
- 开发语言:Dart
- 开发框架:Flutter
- 调试设备:OpenHarmony 手机模拟器
- 适配平台:OpenHarmony
2. 依赖配置
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
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
final List<Widget> _pages = const [
HomePage(),
HealthInputPage(),
ProfilePage(),
AboutPage(),
];
void _onItemTapped(int index) {
setState(() {
_currentIndex = index;
});
}
Future<bool> _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
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
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 = "肥胖";
}
}
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") ?? "未填写";
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
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 = "男";
String _getNowTime() {
DateTime now = DateTime.now();
return "${now.year}-${now.month}-${now.day} ${now.hour}:${now.minute}";
}
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;
}
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
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends widget<ProfilePage> {
String name = "未填写";
String gender = "未填写";
String age = "未填写";
String height = "未填写";
String weight = "未填写";
String heart = "未填写";
String saveTime = "暂无记录时间";
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") ?? "未填写";
saveTime = prefs.getString("saveTime") ?? "暂无记录时间";
});
}
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: 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 快速实现可视化需求。
- 点赞收藏不迷路,后续每日开发笔记将持续同步更新!
更多推荐



所有评论(0)