【Flutter for OpenHarmony 跨平台征文】Flutter 血压趋势统计实战:从统计数据计算到简化柱状图的鸿蒙开发指南

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


🎯 写在前面

嗨,大家好!我是上海某高校大一计算机专业的学生 🚀,专注 Flutter for OpenHarmony 跨平台开发~

前面三篇我们已经完整实现:
✅ 血压数据模型 + WHO 分类算法
✅ 血压录入表单 + 实时预览
✅ 历史记录列表 + 空状态设计

这是血压系列收官篇趋势统计 + 简化柱状图 📊
全程 100% Flutter(Dart) 代码,无 ArkUI、无 ETS、无鸿蒙原生API
一套代码直接跑 OpenHarmony + Android,真正跨平台!


一、趋势统计需求分析

1.1 功能需求

需求 说明
统计卡片 平均收缩压 / 平均舒张压 / 综合评估
趋势图表 最近7天双柱状图(收缩压+舒张压)
数据列表 本周明细 + 状态颜色标签
空数据兼容 无记录时不崩溃、显示友好提示
跨平台运行 Flutter 编译鸿蒙 Hap 直接使用

1.2 技术方案(Flutter 原生实现)

不引入任何第三方图表库!
使用 Flutter 原生组件Container + Row + Column 实现轻量柱状图
✅ 体积小
✅ 性能高
✅ 鸿蒙/安卓双端完美兼容


二、完整 Flutter 代码实现(鸿蒙直接运行)

2.1 趋势页面主结构

import 'package:flutter/material.dart';
import '../model/blood_pressure_model.dart';

class BloodPressureTrendPage extends StatefulWidget {
  final List<BloodPressureRecord> records;

  const BloodPressureTrendPage({
    super.key,
    required this.records,
  });

  
  State<BloodPressureTrendPage> createState() => _BloodPressureTrendPageState();
}

class _BloodPressureTrendPageState extends State<BloodPressureTrendPage> {
  late Map<String, num> _avgData;

  
  void initState() {
    super.initState();
    _avgData = _calculateAverage();
  }

  // 计算一周平均血压
  Map<String, num> _calculateAverage() {
    if (widget.records.isEmpty) {
      return {'systolic': 0, 'diastolic': 0, 'pulse': 0};
    }
    final list = widget.records.take(7).toList();
    final sumSys = list.fold(0, (a, b) => a + b.systolic);
    final sumDia = list.fold(0, (a, b) => a + b.diastolic);
    final sumPul = list.fold(0, (a, b) => a + b.pulse);
    return {
      'systolic': (sumSys / list.length).round(),
      'diastolic': (sumDia / list.length).round(),
      'pulse': (sumPul / list.length).round(),
    };
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F7FA),
      appBar: AppBar(
        title: const Text("血压趋势统计"),
        backgroundColor: const Color(0xFF6366F1),
        centerTitle: true,
        elevation: 0,
      ),
      body: widget.records.isEmpty
          ? const _EmptyChartView()
          : SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Column(
                children: [
                  // 平均统计卡片
                  _buildAverageCard(),
                  const SizedBox(height: 16),
                  // 趋势柱状图
                  _buildChartCard(),
                  const SizedBox(height: 16),
                  // 本周数据列表
                  _buildWeekDataList(),
                ],
              ),
            ),
    );
  }
}

2.2 平均血压统计卡片

  // 平均血压卡片
  Widget _buildAverageCard() {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Row(
            children: [
              Text("📊 本周平均血压", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            ],
          ),
          const SizedBox(height: 20),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildStatItem(
                value: _avgData['systolic'].toString(),
                label: "平均收缩压",
                color: const Color(0xFFF44336),
              ),
              _buildStatItem(
                value: _avgData['diastolic'].toString(),
                label: "平均舒张压",
                color: const Color(0xFF2196F3),
              ),
              _buildStatItem(
                value: "${_avgData['systolic']}/${_avgData['diastolic']}",
                label: "综合评估",
                color: Colors.black87,
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildStatItem({
    required String value,
    required String label,
    required Color color,
  }) {
    return Column(
      children: [
        Text(
          value,
          style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: color),
        ),
        const SizedBox(height: 4),
        Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
      ],
    );
  }

2.3 Flutter 原生简化柱状图(核心)

  // 血压趋势柱状图
  Widget _buildChartCard() {
    final list = widget.records.take(7).toList();

    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        children: [
          const Row(
            children: [
              Text("📈 本周血压趋势", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            ],
          ),
          const SizedBox(height: 20),
          // 柱状图
          Row(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: List.generate(list.length, (index) {
              final item = list[index];
              final sysHeight = _normalizeSys(item.systolic);
              final diaHeight = _normalizeDia(item.diastolic);
              final week = _getWeekLabel(index);

              return Expanded(
                child: Column(
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: [
                        // 收缩压(红)
                        Container(
                          width: 12,
                          height: sysHeight,
                          decoration: BoxDecoration(
                            color: const Color(0xFFF44336),
                            borderRadius: BorderRadius.circular(3),
                          ),
                        ),
                        const SizedBox(width: 4),
                        // 舒张压(橙)
                        Container(
                          width: 12,
                          height: diaHeight,
                          decoration: BoxDecoration(
                            color: const Color(0xFFFF9800),
                            borderRadius: BorderRadius.circular(3),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 6),
                    Text(week, style: const TextStyle(fontSize: 11, color: Colors.grey)),
                  ],
                ),
              );
            }),
          ),
          const SizedBox(height: 16),
          // 图例
          const Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              _Legend(color: Color(0xFFF44336), text: "收缩压"),
              SizedBox(width: 24),
              _Legend(color: Color(0xFFFF9800), text: "舒张压"),
            ],
          ),
        ],
      ),
    );
  }

  // 归一化:收缩压 → 高度
  double _normalizeSys(int value) {
    const minH = 20.0, maxH = 80.0;
    double norm = (value - 90) / 120;
    double h = minH + norm * (maxH - minH);
    return h.clamp(minH, maxH);
  }

  // 归一化:舒张压 → 高度
  double _normalizeDia(int value) {
    const minH = 20.0, maxH = 80.0;
    double norm = (value - 50) / 80;
    double h = minH + norm * (maxH - minH);
    return h.clamp(minH, maxH);
  }

  // 星期文字
  String _getWeekLabel(int i) {
    const list = ['一', '二', '三', '四', '五', '六', '日'];
    return list[i];
  }

2.4 本周数据列表

  // 本周数据列表
  Widget _buildWeekDataList() {
    final list = widget.records.take(7).toList();
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        children: [
          const Row(
            children: [
              Text("📋 本周数据", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            ],
          ),
          const SizedBox(height: 12),
          ...List.generate(list.length, (index) {
            final item = list[index];
            return Padding(
              padding: const EdgeInsets.symmetric(vertical: 8),
              child: Row(
                children: [
                  Text(item.formattedDate, style: const TextStyle(fontSize: 13, color: Colors.grey)),
                  const SizedBox(width: 12),
                  Text(
                    "${item.systolic}/${item.diastolic}",
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: HexColor(item.status.color)),
                  ),
                  const Spacer(),
                  Text("${item.pulse} bpm", style: const TextStyle(fontSize: 12, color: Colors.grey)),
                  const SizedBox(width: 8),
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      color: HexColor(item.status.color).withOpacity(0.1),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Text(
                      item.status.text,
                      style: TextStyle(color: HexColor(item.status.color), fontSize: 12),
                    ),
                  ),
                ],
              ),
            );
          }),
        ],
      ),
    );
  }

2.5 空状态 / 图例工具类

// 空图表状态
class _EmptyChartView extends StatelessWidget {
  const _EmptyChartView();
  
  Widget build(BuildContext context) {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text("📊", style: TextStyle(fontSize: 64)),
          SizedBox(height: 16),
          Text("暂无统计数据", style: TextStyle(fontSize: 16, color: Colors.grey)),
        ],
      ),
    );
  }
}

// 图例组件
class _Legend extends StatelessWidget {
  final Color color;
  final String text;
  const _Legend({super.key, required this.color, required this.text});

  
  Widget build(BuildContext context) {
    return Row(
      children: [
        Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
        const SizedBox(width: 6),
        Text(text, style: const TextStyle(fontSize: 12, color: Colors.grey)),
      ],
    );
  }
}

// 十六进制颜色工具
class HexColor extends Color {
  static int _getColorFromHex(String hexColor) {
    hexColor = hexColor.toUpperCase().replaceAll("#", "");
    if (hexColor.length == 6) hexColor = "FF$hexColor";
    return int.parse(hexColor, radix: 16);
  }
  HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}

三、Flutter 数据统计核心逻辑

3.1 平均值计算

final list = widget.records.take(7).toList();
final sumSys = list.fold(0, (a, b) => a + b.systolic);
final avg = (sumSys / list.length).round();

3.2 图表归一化算法(数值→高度)

double _normalizeSys(int value) {
  const minHeight = 20.0;
  const maxHeight = 80.0;
  double normalized = (value - 90) / 120;
  double height = minHeight + normalized * (maxHeight - minHeight);
  return height.clamp(minHeight, maxHeight);
}

✅ 自动适配视觉高度
✅ 防止极端值溢出


四、为什么这是 Flutter for OpenHarmony(不是纯鸿蒙)

你可以直接放在文章里,官方征文专用说明

✅ 100% Flutter 技术栈

  • 语言:Dart
  • 框架:Flutter 3.x
  • 无 ArkUI / 无 ETS / 无鸿蒙原生API
  • 无鸿蒙专属装饰器(如 @Builder、@State)

✅ 真正跨平台编译

一套代码直接编译:

  • Android APK
  • OpenHarmony HAP(鸿蒙安装包)

✅ 平台体验完全一致

列表、图表、统计、颜色、交互
鸿蒙设备 = 安卓手机 完全相同


五、Flutter 开发踩坑记录

坑1:柱状图高度不对

✅ 解决:使用 归一化算法 + clamp 限制范围

坑2:图表从中间向上长

✅ 解决:使用 CrossAxisAlignment.end 底部对齐

坑3:无数据崩溃

✅ 解决:优先判断 records.isEmpty 显示空状态

坑4:列表不刷新

✅ 解决:initState 中预计算,更新数据后 setState


六、功能验证清单(鸿蒙真机测试)

功能 效果 状态
平均统计 正确计算一周平均值
双柱状图 红=收缩压 / 橙=舒张压
柱高映射 数值越大,柱子越高
本周列表 显示日期、血压、脉搏、状态
空状态 无数据显示友好页面
鸿蒙运行 无闪退、无错位

七、个人总结

作为大一学生,用 Flutter 实现跨平台健康 App 真的收获巨大!

通过这四篇实战,我完整掌握了:

  • Flutter 表单与状态管理
  • Flutter 列表与空状态设计
  • Flutter 原生图表绘制(不用第三方库)
  • Flutter for OpenHarmony 跨平台编译

图表并没有想象中难!
用 Flutter 最基础的组件就能做出美观、实用的趋势图~


八、血压系列完整收官 🎉

✅ 数据模型
✅ 录入表单
✅ 历史列表
✅ 趋势统计(本篇)

全套 Flutter 跨平台代码 可直接用于:

  • 课程设计
  • 毕业设计
  • 健康类跨平台 App
  • Flutter for OpenHarmony 学习示范

再次感谢大家阅读,我们下个系列再见!


创作主题:Flutter for OpenHarmony 跨平台开发
技术栈:Flutter(Dart)
适用平台:OpenHarmony、Android
作者:上海大一计算机专业学生
版权:转载注明出处
在这里插入图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐