Flutter 跨平台实战:OpenHarmony 健康管理应用 Day17|历史数据本地持久化与多记录存储实现
大家好,本篇是 Flutter+OpenHarmony 健康管理应用开发系列第十七篇笔记。在 Day15 引入 fl_chart 图表依赖、Day16 完成健康数据折线图绘制的基础上,今日实现多条健康数据本地持久化存储、历史记录列表展示、新增数据追加存储功能,解决原有单条数据覆盖问题,适配 OpenHarmony 鸿蒙环境稳定运行,代码可直接复制使用,与前文功能无缝衔接、不脱节。
🎯Flutter 跨平台实战:OpenHarmony 健康管理应用 Day17|历史数据本地持久化与多记录存储实现
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🚀前言
大家好,本篇是 Flutter+OpenHarmony 健康管理应用开发系列第十七篇笔记。在 Day15 引入 fl_chart 图表依赖、Day16 完成健康数据折线图绘制的基础上,今日实现多条健康数据本地持久化存储、历史记录列表展示、新增数据追加存储功能,解决原有单条数据覆盖问题,适配 OpenHarmony 鸿蒙环境稳定运行,代码可直接复制使用,与前文功能无缝衔接、不脱节。
💥 本文你能学到
-
基于 SharedPreferences 实现多条健康数据 JSON 数组存储
-
历史数据列表页面布局设计与数据渲染
-
新增数据自动追加、避免覆盖原有记录,同步支撑折线图数据展示
-
保留前期所有功能:表单校验、BMI 计算、页面动画、退出弹窗、fl_chart 图表展示、关于页面
-
统一全局 UI 风格,兼容鸿蒙模拟器无布局错乱,确保与前面功能衔接流畅
🥝 开发环境
环境信息
-
开发工具:DevEco Studio
-
开发语言:Dart
-
开发框架:Flutter
-
调试设备:OpenHarmony 手机模拟器
-
适配平台:OpenHarmony
依赖配置
沿用 Day15 引入的 fl_chart 图表依赖,新增 JSON 序列化相关处理(无需额外新增第三方库),确保与前文依赖衔接,同时适配鸿蒙环境无冲突
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.2.2
fl_chart: ^0.55.2
📝 今日核心开发功能
-
改造本地存储逻辑,支持多条健康数据以 JSON 格式持久化保存,为 Day16 折线图提供多组数据支撑
-
新增历史记录页面,展示所有已录入的健康信息,与折线图数据同源、同步更新
-
录入新数据时自动追加至列表,不再覆盖原有数据,同时同步更新折线图展示内容
-
底部导航新增「历史记录」Tab,实现五页面平滑切换,与首页、图表相关页面衔接流畅
-
完全兼容鸿蒙系统,保留 fl_chart 依赖且无适配冲突,运行稳定无闪退
-
保留项目全部已有功能,不修改原有代码结构,确保与前面功能无缝衔接
✅ 完整可运行代码
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:fl_chart/fl_chart.dart'; // 还原Day15引入的fl_chart依赖,衔接前文图表功能
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 HealthRecord {
final String name;
final String gender;
final String age;
final String height;
final String weight;
final String heart;
final String saveTime;
HealthRecord({
required this.name,
required this.gender,
required this.age,
required this.height,
required this.weight,
required this.heart,
required this.saveTime,
});
// JSON转对象
factory HealthRecord.fromJson(Map<String, dynamic> json) {
return HealthRecord(
name: json['name'],
gender: json['gender'],
age: json['age'],
height: json['height'],
weight: json['weight'],
heart: json['heart'],
saveTime: json['saveTime'],
);
}
// 对象转JSON
Map<String, dynamic> toJson() {
return {
'name': name,
'gender': gender,
'age': age,
'height': height,
'weight': weight,
'heart': heart,
'saveTime': saveTime,
};
}
}
// 新增:图表数据处理工具类(衔接Day16折线图功能)
class ChartDataUtil {
// 从历史记录中提取体重数据,用于折线图展示
static List<FlSpot> getWeightSpots(List<HealthRecord> records) {
List<FlSpot> spots = [];
for (int i = 0; i < records.length; i++) {
double weight = double.tryParse(records[i].weight) ?? 0.0;
spots.add(FlSpot(i.toDouble(), weight));
}
return spots;
}
// 从历史记录中提取心率数据,用于折线图展示
static List<FlSpot> getHeartSpots(List<HealthRecord> records) {
List<FlSpot> spots = [];
for (int i = 0; i < records.length; i++) {
double heart = double.tryParse(records[i].heart) ?? 0.0;
spots.add(FlSpot(i.toDouble(), heart));
}
return spots;
}
}
// 新增:图表展示页面(沿用Day16折线图逻辑,与历史记录数据联动)
class ChartPage extends StatefulWidget {
const ChartPage({super.key});
@override
State<ChartPage> createState() => _ChartPageState();
}
class _ChartPageState extends State<ChartPage> {
List<HealthRecord> recordList = [];
int _selectedTab = 0; // 0:体重折线图,1:心率折线图
Future<void> _loadHistoryRecords() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? recordStr = prefs.getString("health_records");
if (recordStr != null) {
List<dynamic> jsonList = json.decode(recordStr);
setState(() {
recordList = jsonList.map((e) => HealthRecord.fromJson(e)).toList();
});
}
}
@override
void initState() {
super.initState();
_loadHistoryRecords();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("健康数据图表")),
body: Column(
children: [
// 图表切换Tab
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _selectedTab == 0 ? Colors.teal : Colors.grey[200],
foregroundColor: _selectedTab == 0 ? Colors.white : Colors.black,
),
onPressed: () => setState(() => _selectedTab = 0),
child: const Text("体重趋势"),
),
const SizedBox(width: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _selectedTab == 1 ? Colors.teal : Colors.grey[200],
foregroundColor: _selectedTab == 1 ? Colors.white : Colors.black,
),
onPressed: () => setState(() => _selectedTab = 1),
child: const Text("心率趋势"),
),
],
),
const SizedBox(height: 20),
// 折线图展示
Expanded(
child: recordList.isEmpty
? const Center(child: Text("暂无健康数据,无法展示图表", style: TextStyle(fontSize: 16, color: Colors.grey)))
: _selectedTab == 0
? _buildWeightChart()
: _buildHeartChart(),
),
],
),
);
}
// 体重折线图(沿用Day16逻辑)
Widget _buildWeightChart() {
List<FlSpot> spots = ChartDataUtil.getWeightSpots(recordList);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: LineChart(
LineChartData(
gridData: const FlGridData(show: true, drawBorder: false),
titlesData: const FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(showTitles: true, interval: 1, reservedSize: 20),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: true, interval: 5, reservedSize: 40),
),
),
borderData: FlBorderData(show: true),
minX: 0,
maxX: spots.length > 0 ? spots.last.x : 0,
minY: spots.isNotEmpty ? spots.map((e) => e.y).reduce((a, b) => a < b ? a : b) - 5 : 0,
maxY: spots.isNotEmpty ? spots.map((e) => e.y).reduce((a, b) => a > b ? a : b) + 5 : 100,
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: Colors.teal,
thickness: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(show: true, color: Colors.teal.withOpacity(0.2)),
),
],
),
),
);
}
// 心率折线图(沿用Day16逻辑)
Widget _buildHeartChart() {
List<FlSpot> spots = ChartDataUtil.getHeartSpots(recordList);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: LineChart(
LineChartData(
gridData: const FlGridData(show: true, drawBorder: false),
titlesData: const FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(showTitles: true, interval: 1, reservedSize: 20),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: true, interval: 20, reservedSize: 40),
),
),
borderData: FlBorderData(show: true),
minX: 0,
maxX: spots.length > 0 ? spots.last.x : 0,
minY: 40,
maxY: 180,
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: Colors.redAccent,
thickness: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(show: true, color: Colors.redAccent.withOpacity(0.2)),
),
],
),
),
);
}
}
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(),
ChartPage(), // 还原Day16的图表页面,与历史记录页面衔接
HistoryRecordPage(),
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.bar_chart), label: "数据图表"), // 还原图表Tab,衔接前文
BottomNavigationBarItem(icon: Icon(Icons.history), 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: 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(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);
// 多条历史记录追加存储,同步更新图表数据
List<HealthRecord> records = [];
String? recordStr = prefs.getString("health_records");
if (recordStr != null) {
List<dynamic> jsonList = json.decode(recordStr);
records = jsonList.map((e) => HealthRecord.fromJson(e)).toList();
}
records.add(HealthRecord(
name: name,
gender: _gender,
age: ageStr,
height: heightStr,
weight: weightStr,
heart: heartStr,
saveTime: nowTime,
));
await prefs.setString("health_records", json.encode(records.map((e) => e.toJson()).toList()));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("保存成功,图表已同步更新")));
// 清空输入框
_nameController.clear();
_ageController.clear();
_heightController.clear();
_weightController.clear();
_heartController.clear();
}
@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 HistoryRecordPage extends StatefulWidget {
const HistoryRecordPage({super.key});
@override
State<HistoryRecordPage> createState() => _HistoryRecordPageState();
}
class _HistoryRecordPageState extends State<HistoryRecordPage> {
List<HealthRecord> recordList = [];
Future<void> _loadHistoryRecords() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? recordStr = prefs.getString("health_records");
if (recordStr != null) {
List<dynamic> jsonList = json.decode(recordStr);
setState(() {
recordList = jsonList.map((e) => HealthRecord.fromJson(e)).toList();
});
}
}
@override
void initState() {
super.initState();
_loadHistoryRecords();
}
Widget _buildRecordItem(HealthRecord record) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("录入时间:${record.saveTime}", style: TextStyle(color: Colors.grey[600])),
SizedBox(height: 10),
Text("姓名:${record.name} 性别:${record.gender}"),
SizedBox(height: 6),
Text("年龄:${record.age}岁 身高:${record.height}cm"),
SizedBox(height: 6),
Text("体重:${record.weight}kg 心率:${record.heart}次/分"),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("历史记录")),
body: recordList.isEmpty
? Center(child: Text("暂无历史健康记录,录入数据后可同步展示图表", style: TextStyle(fontSize: 16, color: Colors.grey)))
: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 10),
itemCount: recordList.length,
itemBuilder: (context, index) => _buildRecordItem(recordList[index]),
),
);
}
}
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 = "未填写";
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.7",
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体质指数自动计算、本地数据持久化存储、多条历史记录保存、fl_chart健康数据折线图展示、录入时间记录、全局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 本地存储 + JSON序列化 + fl_chart图表",
style: TextStyle(fontSize: 15),
),
SizedBox(height: 20),
Text("开发用途", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 12),
Text(
"课程实训综合项目,完整覆盖页面布局、表单校验、多条数据存储、JSON序列化、列表渲染、图表展示、业务逻辑、UI美化、交互优化等核心开发知识点,功能连贯、结构完整。",
style: TextStyle(fontSize: 15, height: 1.6),
),
],
),
),
),
],
),
),
);
}
}
📱 调试与运行完整步骤
-
停止原有项目运行
-
添加 fl_chart 依赖(已在配置中写好),终端执行 flutter pub get
flutter clean flutter pub get -
修改 lib\main.dart 代码,无需修改其他文件
-
连接鸿蒙模拟器,执行 flutter run
flutter run -
底部导航切换「首页」「历史记录」页面,验证数据同步展示


-
多次录入健康数据,验证数据自动追加、图表同步更新、历史记录正常显示
-
测试五个页面切换、动画、退出弹窗、数据存储、BMI计算、图表展示全部正常



-
布局适配鸿蒙,无错位、无遮挡、无依赖冲突
🔐 跨平台适配说明
本次保留 Day15 引入的 fl_chart: ^0.55.2 版本(经测试适配鸿蒙模拟器,无闪退、无编译报错),新增历史记录页面与 JSON 持久化逻辑,与 Day16 功能无缝衔接。所有功能完全遵循鸿蒙Flutter开发规范,沿用原有SharedPreferences稳定方案,在OpenHarmony模拟器中运行流畅,完美兼容历史所有功能,确保上下文衔接不脱节。
❌ 常见错误排查
|
错误现象 |
解决方法 |
|---|---|
|
历史记录不显示、图表无数据 |
检查JSON序列化/反序列化逻辑,确保数据正确编码存储;执行flutter pub get 确认fl_chart依赖加载成功 |
|
底部导航挤压文字 |
保持BottomNavigationBarType.fixed模式,适配多Tab显示 |
|
数据仍被覆盖 |
确认先读取原有记录再追加,而非直接覆盖赋值;检查saveData方法中JSON数组处理逻辑 |
|
图表无法渲染、报错 |
确保fl_chart版本为^0.55.2,与鸿蒙环境适配;检查ChartDataUtil工具类中数据转换逻辑,避免空数据导致报错 |
🎨 项目后续规划
Day17完成多条健康数据持久化、历史记录展示,同时还原fl_chart图表功能,确保与Day16衔接流畅;Day18将进行最终代码精简、项目总结与结业收尾,优化细节、梳理完整开发流程,整套健康管理项目正式闭环。
📌 项目总结
本篇Day17在Day16的基础上,实现JSON格式多条健康数据本地存储、历史记录列表展示、数据追加保存功能,同时还原fl_chart图表依赖与图表页面,确保各功能模块无缝衔接、上下文连贯。底部导航升级为六页面结构,彻底解决原有单条数据覆盖问题,项目数据层、展示层能力大幅完善,同时保持与鸿蒙系统高度兼容,为最终Day18项目收尾奠定基础。
✅ 结尾小贴士
- 多条数据存储必须使用JSON数组序列化处理,直接覆盖会丢失历史数据
- fl_chart依赖需使用适配鸿蒙的版本(如^0.55.2),避免版本过高导致适配冲突
- 底部导航多Tab必须配置fixed模式,保证鸿蒙设备UI显示正常
- 历史记录与图表数据同源,确保数据同步更新、衔接流畅
- 点赞收藏不迷路,最后一日开发笔记持续同步更新
更多推荐


所有评论(0)