当前位置: 首页 > news >正文

Flutter 跨平台实战:OpenHarmony 健康管理应用 Day9|首页 UI 美化、个人信息展示与功能快捷导航

🎯Flutter 跨平台实战:OpenHarmony 健康管理应用 Day9|首页 UI 美化、个人信息展示与功能快捷导航

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

🚀 前言

大家好,本篇是我真实完成Flutter + OpenHarmony 健康管理应用的 Day9 开发笔记,基于 Day1-Day8 已经完成项目初始化、表单录入、页面路由、本地持久化存储、数据图表可视化、底部导航整体框架搭建等功能。今天主要对应用首页进行全面 UI 美化改造,实现欢迎信息卡片展示、个人信息自动读取、功能快捷导航跳转、健康温馨提示面板,整体采用卡片圆角 + 阴影布局,页面层次更分明、界面更贴近商用 APP 风格,全程只用 DevEco Studio 开发,一步不漏、全部写细,新手照着做就能一次成功!

我会把首页布局重构、卡片组件使用、异步读取本地数据、跨 Tab 快捷跳转、界面美化适配的全过程记录详细,照着做就能掌握 Flutter 页面美化与模块化布局开发技巧。

💥 本文你能学到!!!!!!全部都是干货!!!!!!

  1. Flutter Card 卡片圆角、阴影、内边距完整美化用法
  2. FutureBuilder 异步读取本地缓存数据并动态渲染 UI
  3. 首页模块化拆分:欢迎区、功能导航区、温馨提示区
  4. 通过上下文查找导航状态,实现首页点击快捷切换底部 Tab
  5. 已有项目无损迭代美化,保留之前所有业务功能
  6. 多组件组合排版、间距适配、页面滑动适配技巧
  7. OpenHarmony 跨平台 UI 统一美化适配规范

🥝 开发环境与项目准备

1. 开发环境

  • 开发工具:DevEco Studio(全程仅使用这一个工具,不使用其他编辑器)
  • 开发语言:Dart
  • 框架:Flutter(OpenHarmony 适配版本)
  • 调试方式:Web 浏览器端调试(避开模拟器配置繁琐问题,完整验证首页美化、数据展示与页面跳转功能)
  • 后续适配:OpenHarmony 模拟器及真机,当前 UI 美化代码可直接兼容.hap打包发布

2. 项目准备

基于 Day8 已完成底部导航框架的项目,无需新建工程,直接在原有项目上迭代开发:

  1. 已完成三页面底部导航、健康数据录入、本地存储、图表展示功能
  2. 项目依赖无缺失、可正常编译运行
  3. 无需新增第三方依赖,沿用已有依赖库即可
  4. 保留原有工程目录与鸿蒙适配配置,不破坏项目结构

本项目全程基于 Flutter for OpenHarmony 开发,后续继续迭代 UI 细节与功能扩展,本篇为首页美化与快捷导航优化篇。

📝 核心功能:Day9 开发实现内容

1. 功能说明

本次 Day9 迭代开发完成以下核心内容:

  • 首页全新 UI 重构,采用卡片式布局,界面整洁美观
  • 进入首页自动读取本地保存的姓名、年龄、性别并展示欢迎信息
  • 新增功能快捷导航卡片,点击可直接跳转到健康录入、个人中心页面
  • 增加健康温馨提示面板,提升 APP 人性化体验
  • 全部页面保留原有录入、存储、图表、导航切换所有功能
  • 适配页面滚动,内容过多可正常滑动,防止布局溢出
  • 样式统一美化,圆角、阴影、间距规范统一

2. 完整可运行核心代码(lib/main.dart)

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: '鸿蒙健康管理APP', debugShowCheckedModeBanner: false, theme: ThemeData(primarySwatch: Colors.blue), home: const MainBottomNavPage(), ); } } // ========================== // 底部导航主框架 // ========================== class MainBottomNavPage extends StatefulWidget { const MainBottomNavPage({super.key}); @override State<MainBottomNavPage> createState() => _MainBottomNavPageState(); } class _MainBottomNavPageState extends State<MainBottomNavPage> { int _currentIndex = 0; final List<Widget> _pageList = const [ HomePage(), HealthInputPage(), ProfilePage(), ]; @override Widget build(BuildContext context) { return Scaffold( body: _pageList[_currentIndex], bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { setState(() { _currentIndex = index; }); }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'), BottomNavigationBarItem(icon: Icon(Icons.add_box), label: '健康录入'), BottomNavigationBarItem(icon: Icon(Icons.person), label: '个人中心'), ], ), ); } } // ========================== // 首页(Day9 美化版) // ========================== class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('首页'), centerTitle: true, ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: FutureBuilder<SharedPreferences>( future: SharedPreferences.getInstance(), builder: (ctx, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } final sp = snapshot.data!; String name = sp.getString('name') ?? '未设置'; String age = sp.getString('age') ?? ''; String gender = sp.getString('gender') ?? ''; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 欢迎面板 Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '👋 欢迎回来', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( '姓名:$name', style: const TextStyle(fontSize: 16), ), if (age.isNotEmpty) Text('年龄:$age 岁', style: const TextStyle(fontSize: 16)), if (gender.isNotEmpty) Text('性别:$gender', style: const TextStyle(fontSize: 16)), ], ), ), ), const SizedBox(height: 20), // 功能卡片 const Text( '📋 功能导航', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), Card( child: ListTile( leading: const Icon(Icons.add_chart, color: Colors.blue), title: const Text('健康数据录入'), subtitle: const Text('记录身高、体重、心率信息'), onTap: () { final nav = context.findAncestorStateOfType<_MainBottomNavPageState>(); nav?.setState(() { nav._currentIndex = 1; }); }, ), ), Card( child: ListTile( leading: const Icon(Icons.person, color: Colors.green), title: const Text('个人中心'), subtitle: const Text('查看健康数据与图表'), onTap: () { final nav = context.findAncestorStateOfType<_MainBottomNavPageState>(); nav?.setState(() { nav._currentIndex = 2; }); }, ), ), const SizedBox(height: 20), // 健康提示 const Card( color: Colors.lightBlueAccent, child: Padding( padding: EdgeInsets.all(16.0), child: Text( '💡 温馨提示:定期记录健康数据,关注身体变化,保持健康生活!', style: TextStyle(color: Colors.white, fontSize: 14), ), ), ), ], ); }, ), ), ); } } // ========================== // 健康录入页面 // ========================== class HealthInputPage extends StatefulWidget { const HealthInputPage({super.key}); @override State<HealthInputPage> createState() => _HealthInputPageState(); } class _HealthInputPageState extends State<HealthInputPage> { final formKey = GlobalKey<FormState>(); final nameCtrl = TextEditingController(); final ageCtrl = TextEditingController(); final heightCtrl = TextEditingController(); final weightCtrl = TextEditingController(); final heartCtrl = TextEditingController(); String? gender; @override void initState() { super.initState(); loadData(); } Future<void> loadData() async { final sp = await SharedPreferences.getInstance(); setState(() { nameCtrl.text = sp.getString("name") ?? ""; ageCtrl.text = sp.getString("age") ?? ""; gender = sp.getString("gender"); heightCtrl.text = sp.getString("height") ?? ""; weightCtrl.text = sp.getString("weight") ?? ""; heartCtrl.text = sp.getString("heart") ?? ""; }); } Future<void> saveData() async { final sp = await SharedPreferences.getInstance(); sp.setString("name", nameCtrl.text); sp.setString("age", ageCtrl.text); sp.setString("gender", gender ?? ""); sp.setString("height", heightCtrl.text); sp.setString("weight", weightCtrl.text); sp.setString("heart", heartCtrl.text); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('健康录入'), centerTitle: true), body: Padding( padding: const EdgeInsets.all(20), child: SingleChildScrollView( child: Form( key: formKey, child: Column( children: [ TextFormField( controller: nameCtrl, decoration: const InputDecoration( labelText: '姓名', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: ageCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '年龄', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), DropdownButtonFormField<String>( value: gender, decoration: const InputDecoration( labelText: '性别', border: OutlineInputBorder(), ), items: const [ DropdownMenuItem(value: "男", child: Text("男")), DropdownMenuItem(value: "女", child: Text("女")), ], onChanged: (val) => setState(() => gender = val), ), const SizedBox(height: 12), TextFormField( controller: heightCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '身高(cm)', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: weightCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '体重(kg)', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: heartCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '心率', border: OutlineInputBorder(), ), ), const SizedBox(height: 30), ElevatedButton( onPressed: () async { if (formKey.currentState!.validate()) { await saveData(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('数据保存成功!')), ); } }, child: const Text('保存数据'), ), ], ), ), ), ), ); } } // ========================== // 个人中心 // ========================== class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('个人中心'), centerTitle: true), body: FutureBuilder<SharedPreferences>( future: SharedPreferences.getInstance(), builder: (ctx, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } final sp = snapshot.data!; double h = double.tryParse(sp.getString("height") ?? "0") ?? 0; double w = double.tryParse(sp.getString("weight") ?? "0") ?? 0; double hr = double.tryParse(sp.getString("heart") ?? "0") ?? 0; return SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('个人信息', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), const SizedBox(height: 10), Text("姓名:${sp.getString('name') ?? ''}", style: const TextStyle(fontSize: 16)), Text("年龄:${sp.getString('age') ?? ''}", style: const TextStyle(fontSize: 16)), Text("性别:${sp.getString('gender') ?? ''}", style: const TextStyle(fontSize: 16)), const SizedBox(height: 20), const Text('健康指标', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), const SizedBox(height: 10), Text("身高:${sp.getString('height') ?? 0} cm", style: const TextStyle(fontSize: 16)), Text("体重:${sp.getString('weight') ?? 0} kg", style: const TextStyle(fontSize: 16)), Text("心率:${sp.getString('heart') ?? 0} 次/分", style: const TextStyle(fontSize: 16)), const SizedBox(height: 30), const Text('健康数据图表', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 20), SizedBox( height: 280, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, titlesData: FlTitlesData(show: true), borderData: FlBorderData(show: false), barGroups: [ BarChartGroupData(x: 1, barRods: [BarChartRodData(toY: h, color: Colors.blue)]), BarChartGroupData(x: 2, barRods: [BarChartRodData(toY: w, color: Colors.green)]), BarChartGroupData(x: 3, barRods: [BarChartRodData(toY: hr, color: Colors.red)]), ], ), ), ), ], ), ); }, ), ); } }

3. 关键逻辑与加分点解析

  • 采用 FutureBuilder 异步获取本地存储,避免页面卡顿与空白
  • Card 组件统一设置圆角与阴影,全局 UI 风格标准化
  • 首页拆分为欢迎模块、功能导航模块、温馨提示模块,结构清晰
  • 通过上下文查找根导航状态,实现无需路由直接切换底部 Tab
  • 所有原有录入、存储、图表、导航功能完全保留,无功能丢失
  • 页面嵌套 SingleChildScrollView,适配不同屏幕,解决内容溢出
  • 未填写信息时做默认值处理,界面不会出现空白异常文字

📱 调试与运行完整流程

  1. 终端执行flutter pub get确保依赖正常加载
  2. 输入flutter run -d web-server启动项目
  3. 进入首页自动加载本地个人信息并展示欢迎卡片
  4. 点击功能导航卡片可直接跳转到对应页面
  5. 进入健康录入填写数据保存,首页信息自动同步更新
  6. 重启应用数据不丢失,首页 UI、跳转、图表全部正常
  7. 页面滑动流畅,卡片样式美观,无报错无布局溢出

🔐 跨平台适配说明

本次 Day9 首页美化代码基于 Flutter 跨平台特性,一套代码多端兼容:

平台运行方式适配说明
Chrome 浏览器端flutter run -d web-server首页卡片、文字排版、跳转功能全部正常适配
OpenHarmony 模拟器 / 真机编译生成.hapUI 样式、卡片圆角阴影、布局排版原生适配,无需额外修改代码即可打包发布

🧩 超全错误排查与解决方案

错误场景解决方案
首页一直转圈不显示内容检查是否正常初始化 SharedPreferences,网络调试模式依赖加载完整
点击功能卡片无反应确认查找的导航状态类名和页面类名完全一致
页面内容溢出报错给外层嵌套 SingleChildScrollView 开启滚动适配
文字排版错乱统一设置 padding、SizedBox 间距,保持布局规范

🎨 项目后续规划

Day9 已完成首页 UI 全面美化、个人信息动态展示与功能快捷导航开发,后续将继续迭代完善个人中心功能扩展、全局主题样式统一、页面细节优化等内容,让整体应用更加精致完整。

📌 项目总结

本篇完整记录了 Day9 对健康管理 APP 首页进行 UI 重构与美化的全过程,熟练使用 Card 卡片布局、FutureBuilder 异步数据加载、页面内跨 Tab 跳转等实用技巧,在完全保留历史所有功能的前提下,大幅提升 APP 界面颜值和交互体验,项目结构更加模块化,为后续更多功能扩展和界面优化打下良好基础。

✅ 结尾小贴士

  • 全篇代码可直接复制替换运行,无需额外修改
  • 全程使用 DevEco Studio 即可完成开发调试,无需切换其他编辑器
  • 点赞收藏不迷路,后续每日开发笔记持续更新!
http://www.jsqmd.com/news/768188/

相关文章:

  • Mac微信防撤回终极指南:3分钟安装WeChatIntercept完整教程
  • Arm Neoverse CMN S3(AE) SF集群与非集群模式解析
  • 给S32K3的中断上个‘闹钟’:手把手配置INTM监控PIT定时器中断响应
  • 别再到处搜了!Android开发者必备的官方网址大全(含AOSP源码、NDK、SDK工具站)
  • 如何快速合并B站缓存视频:终极免费工具使用指南
  • 宝塔面板用户必看:/var/log/journal日志暴涨,教你用logrotate和journalctl轻松瘦身
  • Unity 2D角色控制器避坑指南:为什么你的跳跃代码会让角色卡墙或穿模?
  • 利用快马ai快速原型设计,一键生成微pe环境下的系统自动化部署脚本
  • 3分钟快速上手:Amlogic/Rockchip/Allwinner电视盒子刷Armbian终极指南
  • 如何快速入门 Docker 并进行实操?
  • VITA-E框架:多模态并发处理与实时中断响应技术解析
  • 避开那些坑!用Docker在Ubuntu 20.04上快速搞定OpenHarmony 4.0编译环境
  • ClawHarness智能穿戴设备:从传感器选型到机器人集成全解析
  • 用快马ai五分钟生成ui-ux-pro-max级响应式仪表盘原型
  • 用STM32CubeMX和HAL库搞定匿名上位机V7.12通信(附完整工程源码)
  • 通达信缠论插件:3步实现自动化技术分析,告别手工画线烦恼
  • Dynamo节点包安装与使用保姆级教程:从Orchid到Clockwork,10个包搞定BIM自动化
  • 绿化园林景观公司怎么选?2026园林绿化苗木供应商/园林绿化树苗批发公司实力解析-十强小区绿化苗木机构优选推荐 - 栗子测评
  • 为AI Agent设计的英国公司数据CLI工具:companies-house-cli深度解析
  • ParroT框架:通过数据质控与增强提升大语言模型指令微调效果
  • 从“谁该牺牲”到“如何避免牺牲” ——AI元人文构想对电车难题的原创性解决方案
  • Taotoken 的计费透明性如何让小型工作室清晰规划 AI 绘图提示词服务的预算
  • Hindclaw:基于计算机视觉与输入模拟的跨平台桌面自动化框架实践
  • PMSM无感控制避坑指南:滑模观测器(SMO)的增益调参与滤波设计实战
  • Cortex-R82中断控制器架构与实时系统优化
  • Java Stream统计避坑指南:用mapToDouble处理空值和null时,orElse()和filter()到底怎么选?
  • ChatAir:原生Android AI聊天聚合应用,支持多模型与本地部署
  • 实战指南:基于快马ai生成esp8266与dht11的物联网环境监测站代码
  • 汇编语言里的标签(label)到底怎么用?新手常犯的3个错误和正确写法
  • 如何应对GTA5线上模式重复性任务的完整解决方案