【Flutter for OpenHarmony 跨平台征文】Flutter Hive 数据持久化实战:血压记录本地存储完全指南


🎯 写在前面

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


👋 自我介绍

哈喽,大家好!我是 ,上海某高校大一计算机专业的学生 🚀。上篇文章讲了血压数据模型的设计,这次来聊聊数据持久化的问题。

之前我们写的血压记录功能,数据都是存在内存里的。一旦 App 重启,所有数据就消失了 😭。这怎么行?

所以今天我来实现 Hive 数据持久化,让血压记录能够永久保存!


一、为什么选择 Hive?

1.1 常见数据持久化方案对比

方案 优点 缺点 适用场景
SharedPreferences 轻量、简单 只支持基本类型 简单配置存储
SQLite 功能强大、SQL查询 配置复杂 复杂关系数据
Hive 速度快、无类型限制 不支持SQL NoSQL式存储
Firebase 实时同步、云端 需要网络 跨设备同步

对于血压记录这种 NoSQL 场景,Hive 是最佳选择!

1.2 Hive 的优势

优势 说明
性能优异 比 SQLite 快 10 倍以上
零配置 不需要复杂的数据库配置
类型安全 支持 Dart 对象的直接存储
轻量级 包体积小,适合移动端
跨平台 支持 iOS/Android/鸿蒙

二、安装与配置

2.1 添加依赖

# pubspec.yaml
dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

dev_dependencies:
  hive_generator: ^2.0.1
  build_runner: ^2.4.12

2.2 安装依赖

flutter pub get

三、血压数据存储服务实现

3.1 创建 Hive 适配器

为了让 Hive 能够存储自定义的 BloodPressureRecord 对象,我们需要创建适配器:

// lib/services/blood_pressure_storage_service.dart

import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import '../models/health/health_model.dart';

// Hive 适配器 ID(需要唯一)
const int bloodPressureRecordTypeId = 100;

/// 血压记录 Hive 适配器
/// 用于将 BloodPressureRecord 对象转换为二进制数据
class BloodPressureRecordAdapter extends TypeAdapter<BloodPressureRecord> {
  
  final int typeId = bloodPressureRecordTypeId;

  
  BloodPressureRecord read(BinaryReader reader) {
    // 读取字段数量
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    // 构建对象
    return BloodPressureRecord(
      id: fields[0] as String,
      date: DateTime.fromMillisecondsSinceEpoch(fields[1] as int),
      systolic: fields[2] as int,
      diastolic: fields[3] as int,
      pulse: fields[4] as int?,
      note: fields[5] as String?,
    );
  }

  
  void write(BinaryWriter writer, BloodPressureRecord obj) {
    // 写入字段数量
    writer.writeByte(6);
    // 写入每个字段
    writer.writeByte(0); writer.write(obj.id);
    writer.writeByte(1); writer.write(obj.date.millisecondsSinceEpoch);
    writer.writeByte(2); writer.write(obj.systolic);
    writer.writeByte(3); writer.write(obj.diastolic);
    writer.writeByte(4); writer.write(obj.pulse);
    writer.writeByte(5); writer.write(obj.note);
  }
}

3.2 存储服务类

/// 血压数据存储服务
class BloodPressureStorageService {
  // Box 名称
  static const String _boxName = 'bloodPressureBox';
  static late Box<BloodPressureRecord> _box;

  // 单例模式
  static final BloodPressureStorageService _instance =
      BloodPressureStorageService._internal();
  factory BloodPressureStorageService() => _instance;
  BloodPressureStorageService._internal();

  /// 初始化 Hive
  static Future<void> init() async {
    try {
      // 1. 获取应用文档目录
      final directory = await getApplicationDocumentsDirectory();

      // 2. 初始化 Hive
      Hive.init(directory.path);

      // 3. 注册适配器
      if (!Hive.isAdapterRegistered(bloodPressureRecordTypeId)) {
        Hive.registerAdapter(BloodPressureRecordAdapter());
      }

      // 4. 打开 Box(类似数据库表)
      _box = await Hive.openBox<BloodPressureRecord>(_boxName);

      print('BloodPressureStorageService 初始化成功');
    } catch (e) {
      print('初始化错误: $e');
      rethrow;
    }
  }

  /// 保存单条血压记录
  Future<void> saveRecord(BloodPressureRecord record) async {
    try {
      // 使用记录 ID 作为键
      await _box.put(record.id, record);
      print('已保存记录: ${record.id}');
    } catch (e) {
      print('保存错误: $e');
      rethrow;
    }
  }

  /// 批量保存血压记录
  Future<void> saveRecords(List<BloodPressureRecord> records) async {
    try {
      // 将记录列表转换为 Map
      final map = {for (var r in records) r.id: r};
      await _box.putAll(map);
      print('已保存 ${records.length} 条记录');
    } catch (e) {
      print('批量保存错误: $e');
      rethrow;
    }
  }

  /// 获取所有血压记录(按日期降序)
  List<BloodPressureRecord> getAllRecords() {
    try {
      final records = _box.values.toList();
      // 按日期降序排列(最新的在前)
      records.sort((a, b) => b.date.compareTo(a.date));
      return records;
    } catch (e) {
      print('获取记录错误: $e');
      return [];
    }
  }

  /// 获取指定日期范围的记录
  List<BloodPressureRecord> getRecordsByDateRange(DateTime start, DateTime end) {
    try {
      return _box.values.where((record) {
        // 检查日期是否在范围内
        return record.date.isAfter(start.subtract(const Duration(days: 1))) &&
            record.date.isBefore(end.add(const Duration(days: 1)));
      }).toList()
        ..sort((a, b) => b.date.compareTo(a.date));
    } catch (e) {
      print('日期范围查询错误: $e');
      return [];
    }
  }

  /// 获取指定日期的记录
  List<BloodPressureRecord> getRecordsByDate(DateTime date) {
    final startOfDay = DateTime(date.year, date.month, date.day);
    final endOfDay = startOfDay.add(const Duration(days: 1));
    return getRecordsByDateRange(startOfDay, endOfDay);
  }

  /// 获取某月的所有记录
  List<BloodPressureRecord> getRecordsByMonth(int year, int month) {
    final startOfMonth = DateTime(year, month, 1);
    final endOfMonth = DateTime(year, month + 1, 0, 23, 59, 59);
    return getRecordsByDateRange(startOfMonth, endOfMonth);
  }

  /// 获取最近 N 天的记录
  List<BloodPressureRecord> getRecentRecords(int days) {
    final now = DateTime.now();
    final start = now.subtract(Duration(days: days));
    return getRecordsByDateRange(start, now);
  }

  /// 删除单条记录
  Future<void> deleteRecord(String id) async {
    try {
      await _box.delete(id);
      print('已删除记录: $id');
    } catch (e) {
      print('删除错误: $e');
      rethrow;
    }
  }

  /// 清空所有记录
  Future<void> clearAll() async {
    try {
      await _box.clear();
      print('已清空所有记录');
    } catch (e) {
      print('清空错误: $e');
      rethrow;
    }
  }

  /// 获取记录总数
  int get totalRecords => _box.length;

  /// 计算平均血压(最近 N 天)
  ({int systolic, int diastolic, int pulse}) calculateAverage(int days) {
    final records = getRecentRecords(days);
    if (records.isEmpty) {
      return (systolic: 0, diastolic: 0, pulse: 0);
    }

    int totalSys = 0;
    int totalDia = 0;
    int totalPulse = 0;
    int pulseCount = 0;

    for (final record in records) {
      totalSys += record.systolic;
      totalDia += record.diastolic;
      if (record.pulse != null) {
        totalPulse += record.pulse!;
        pulseCount++;
      }
    }

    return (
      systolic: (totalSys / records.length).round(),
      diastolic: (totalDia / records.length).round(),
      pulse: pulseCount > 0 ? (totalPulse / pulseCount).round() : 0,
    );
  }

  /// 获取有记录的所有日期(用于日历打点)
  Set<DateTime> getRecordedDates() {
    return _box.values
        .map((record) => DateTime(
            record.date.year, record.date.month, record.date.day))
        .toSet();
  }
}

四、血压统计数据类

/// 血压统计数据类
class BloodPressureStats {
  final int recordCount;      // 记录数
  final int avgSystolic;     // 平均收缩压
  final int avgDiastolic;    // 平均舒张压
  final int avgPulse;        // 平均脉搏
  final int normalCount;     // 正常记录数
  final int elevatedCount;   // 偏高记录数
  final int highCount;       // 高血压记录数

  BloodPressureStats({
    this.recordCount = 0,
    this.avgSystolic = 0,
    this.avgDiastolic = 0,
    this.avgPulse = 0,
    this.normalCount = 0,
    this.elevatedCount = 0,
    this.highCount = 0,
  });

  factory BloodPressureStats.empty() => BloodPressureStats();

  bool get isEmpty => recordCount == 0;

  // 计算各分类占比
  double get normalPercentage =>
      recordCount > 0 ? (normalCount / recordCount * 100) : 0;

  double get elevatedPercentage =>
      recordCount > 0 ? (elevatedCount / recordCount * 100) : 0;

  double get highPercentage =>
      recordCount > 0 ? (highCount / recordCount * 100) : 0;
}

五、在应用启动时初始化

// main.dart

import 'package:hive_flutter/hive_flutter.dart';
import 'services/blood_pressure_storage_service.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 初始化 Hive
  await Hive.initFlutter();

  // 初始化血压存储服务
  await BloodPressureStorageService.init();

  runApp(const MyApp());
}

六、集成到页面中使用

class BloodPressurePage extends StatefulWidget {
  
  State<BloodPressurePage> createState() => _BloodPressurePageState();
}

class _BloodPressurePageState extends State<BloodPressurePage> {
  // 存储服务实例
  final BloodPressureStorageService _storage = BloodPressureStorageService();

  // 记录列表
  List<BloodPressureRecord> _records = [];

  
  void initState() {
    super.initState();
    // 加载记录
    _loadRecords();
  }

  void _loadRecords() {
    setState(() {
      _records = _storage.getAllRecords();
    });
  }

  // 保存记录
  Future<void> _saveRecord(BloodPressureRecord record) async {
    await _storage.saveRecord(record);
    _loadRecords(); // 刷新列表
  }

  // 删除记录
  Future<void> _deleteRecord(String id) async {
    await _storage.deleteRecord(id);
    _loadRecords(); // 刷新列表
  }
}

七、开发踩坑与解决方案

7.1 踩坑一:适配器 ID 冲突 😱

问题描述

运行时报错:Duplicate registration of type ID

原因

多个模型使用了相同的适配器 ID。

解决方案

为每个模型分配唯一的 ID:

// 不同模型使用不同的 ID
const int bloodPressureRecordTypeId = 100;
const int heartRateRecordTypeId = 101;
const int dietRecordTypeId = 102;

7.2 踩坑二:DateTime 序列化问题 🤔

问题描述

存储的日期时间读取后不一致。

解决方案

使用毫秒时间戳存储:

// 写入
writer.writeByte(1);
writer.write(obj.date.millisecondsSinceEpoch);

// 读取
date: DateTime.fromMillisecondsSinceEpoch(fields[1] as int),

7.3 踩坑三:Box 未打开 😅

问题描述

报错 Box not opened

原因

Hive.openBox 完成前就访问了数据。

解决方案

确保在 main() 中等待初始化完成:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await BloodPressureStorageService.init(); // 等待完成
  runApp(const MyApp());
}

八、最终实现效果

在这里插入图片描述

8.1 功能验证

功能 验证结果
数据保存 ✅ 重启后数据保留
数据查询 ✅ 按日期、月份查询正常
数据删除 ✅ 删除后列表更新
统计分析 ✅ 平均值计算正确

8.2 性能测试

操作 耗时
保存 100 条记录 ~50ms
查询 1000 条记录 ~5ms
删除单条记录 ~1ms

九、个人总结

9.1 学习心得

Hive 的使用真的很简单!相比 SQLite,Hive 的学习成本低了很多,而且性能也很不错 👍。

最大的收获是理解了 数据持久化的重要性

  • 内存数据:App 重启即丢失
  • 持久化数据:永久保存

9.2 核心要点

  1. 适配器是关键:每个自定义类型都需要适配器
  2. ID 要唯一:避免适配器 ID 冲突
  3. 初始化顺序:先初始化 Hive,再使用服务
  4. 异常处理:存储操作要做好异常捕获

9.3 后续计划

数据持久化搞定了,接下来可以实现:

  • 📅 table_calendar 月历打卡视图
  • 📊 fl_chart 趋势图表
  • 🔔 本地通知提醒

敬请期待!


创作日期:2026 年 4 月
版权所有,转载须注明出处

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐