「Flutter三方库sqflite的鸿蒙化适配与实战指南:从入门到踩坑的本地数据库开发全记录」


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


前言:我是谁?为什么写这篇文章?

各位好,我是上海某高校计算机专业的大一学生🏫

前面写了好几篇关于Flutter状态管理、数据可视化和路由的文章,这次终于要讲一个更"底层"的东西了——本地数据库

说实话,之前做的健康运动模块,数据都是存在内存里的,App一重启就全没了。直到加了sqflite数据库,终于实现了数据的持久化存储!

这篇文章就跟大家详细聊聊sqflite在Flutter for OpenHarmony上的适配经历,保证让你的数据稳稳当当的!💾


一、为什么要用sqflite?本地存储方案有哪些?

1.1 Flutter本地存储方案对比

做移动应用,数据持久化是刚需!Flutter给我们提供了好几种本地存储方案:

💾 Flutter本地存储方案
├── SharedPreferences
│   ├── 适用:简单键值对(设置项、主题偏好)
│   └── 限制:不适合大量数据
│
├── sqflite
│   ├── 适用:结构化数据(健康记录、聊天消息)
│   └── 限制:需要SQL知识
│
├── Hive
│   ├── 适用:NoSQL风格、轻量级存储
│   └── 限制:生态不如sqflite成熟
│
└── Drift (formerly Moor)
│   ├── 适用:需要类型安全的SQL
│   └── 限制:编译时代码生成

1.2 为什么选择sqflite?

我的健康运动模块需要存储:

📊 数据存储需求
├── 步数记录(每天一条,大量历史数据)
├── 喝水记录(可能一天多条)
├── 运动记录(包含类型、时长、卡路里)
├── 睡眠记录(时长、质量评分)
└── 需要按日期范围查询

这种结构化的数据,用sqflite是最合适的选择!

1.3 鸿蒙平台上的坑 😤

sqflite在鸿蒙上的兼容性总体还不错,但是有几个地方特别容易踩坑:

问题一:数据库路径拼接错误

路径处理不对,数据库根本创建不成功。

问题二:类型转换出错

DateTime和int类型在存取时出问题。

问题三:索引没建,查询太慢

数据一多,查询卡成PPT。


二,开发前的准备工作:环境和依赖配置

2.1 pubspec.yaml依赖引入

# pubspec.yaml

name: flutter_ohos_health_app
description: Flutter for OpenHarmony 健康运动模块实战
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'
  flutter:
    sdk: flutter

dependencies:
  flutter:
    sdk: flutter
  
  # ==================== 数据库 ====================
  # sqflite - SQLite数据库
  # 【踩坑记录】版本2.3.x在鸿蒙上比较稳定
  sqflite: ^2.3.0
  
  # path - 路径处理
  # 【关键】用于拼接数据库路径
  path: ^1.8.3
  
  # ==================== 状态管理 ====================
  flutter_bloc: ^8.1.3
  bloc: ^8.1.2
  equatable: ^2.0.5
  
  # ==================== 其他依赖 ====================
  provider: ^6.1.0
  shared_preferences: ^2.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

三、分步实现:数据库服务完整代码

3.1 数据模型定义

// lib/models/health/health_model.dart
// 健康数据模型

/// 步数记录模型
/// 存储每日步数统计
class StepRecord {
  final int? id;              // 主键,数据库自增
  final int steps;             // 步数
  final DateTime date;         // 记录日期
  final int goal;              // 目标步数
  final DateTime createdAt;    // 创建时间

  StepRecord({
    this.id,
    required this.steps,
    required this.date,
    this.goal = 10000,
    DateTime? createdAt,
  }) : createdAt = createdAt ?? DateTime.now();

  /// 转换为Map(存入数据库)
  /// 【踩坑记录】DateTime要转成字符串存储
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'steps': steps,
      // 【关键】日期只保留年月日,方便按日期查询
      'date': date.toIso8601String().substring(0, 10),
      'goal': goal,
      'created_at': createdAt.toIso8601String(),
    };
  }

  /// 从Map恢复(从数据库读取)
  factory StepRecord.fromMap(Map<String, dynamic> map) {
    return StepRecord(
      id: map['id'] as int?,
      steps: map['steps'] as int,
      // 【关键】字符串转回DateTime
      date: DateTime.parse(map['date'] as String),
      goal: map['goal'] as int? ?? 10000,
      createdAt: DateTime.parse(map['created_at'] as String),
    );
  }
}

/// 运动类型枚举
enum ExerciseType {
  running('跑步', '🏃', 10),
  swimming('游泳', '🏊', 11),
  cycling('骑行', '🚴', 8),
  gym('健身', '💪', 7),
  yoga('瑜伽', '🧘', 4),
  basketball('篮球', '🏀', 9),
  other('其他', '🎯', 5);

  final String label;    // 显示名称
  final String emoji;    // emoji图标
  final int caloriesPerMinute;  // 每分钟消耗卡路里

  const ExerciseType(this.label, this.emoji, this.caloriesPerMinute);
  
  /// 从字符串恢复枚举
  static ExerciseType fromString(String value) {
    return ExerciseType.values.firstWhere(
      (e) => e.name == value,
      orElse: () => ExerciseType.other,
    );
  }
}

/// 运动记录模型
class ExerciseRecord {
  final int? id;
  final ExerciseType type;
  final int durationMinutes;
  final int calories;
  final DateTime date;
  final DateTime createdAt;

  ExerciseRecord({
    this.id,
    required this.type,
    required this.durationMinutes,
    int? calories,
    DateTime? date,
    DateTime? createdAt,
  })  : calories = calories ?? (type.caloriesPerMinute * durationMinutes),
        date = date ?? DateTime.now(),
        createdAt = createdAt ?? DateTime.now();

  /// 格式化时长显示
  String get formattedDuration {
    final hours = durationMinutes ~/ 60;
    final minutes = durationMinutes % 60;
    if (hours > 0) {
      return '${hours}h ${minutes}min';
    }
    return '${minutes}min';
  }

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      // 【关键】枚举存name,不是label
      'type': type.name,
      'duration_minutes': durationMinutes,
      'calories': calories,
      'date': date.toIso8601String().substring(0, 10),
      'created_at': createdAt.toIso8601String(),
    };
  }

  factory ExerciseRecord.fromMap(Map<String, dynamic> map) {
    return ExerciseRecord(
      id: map['id'] as int?,
      // 【关键】从name恢复枚举
      type: ExerciseType.fromString(map['type'] as String),
      durationMinutes: map['duration_minutes'] as int,
      calories: map['calories'] as int,
      date: DateTime.parse(map['date'] as String),
      createdAt: DateTime.parse(map['created_at'] as String),
    );
  }
}

/// 喝水记录模型
class WaterRecord {
  final int? id;
  final int amount;      // 饮水量(毫升)
  final DateTime date;
  final DateTime createdAt;

  WaterRecord({
    this.id,
    required this.amount,
    DateTime? date,
    DateTime? createdAt,
  })  : date = date ?? DateTime.now(),
        createdAt = createdAt ?? DateTime.now();

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'amount': amount,
      'date': date.toIso8601String().substring(0, 10),
      'created_at': createdAt.toIso8601String(),
    };
  }

  factory WaterRecord.fromMap(Map<String, dynamic> map) {
    return WaterRecord(
      id: map['id'] as int?,
      amount: map['amount'] as int,
      date: DateTime.parse(map['date'] as String),
      createdAt: DateTime.parse(map['created_at'] as String),
    );
  }
}

/// 睡眠记录模型
class SleepRecord {
  final int? id;
  final double hours;
  final int quality;        // 质量1-5
  final DateTime date;
  final DateTime createdAt;

  SleepRecord({
    this.id,
    required this.hours,
    required this.quality,
    DateTime? date,
    DateTime? createdAt,
  })  : date = date ?? DateTime.now(),
        createdAt = createdAt ?? DateTime.now();

  String get qualityLabel {
    switch (quality) {
      case 1: return '很差';
      case 2: return '较差';
      case 3: return '一般';
      case 4: return '较好';
      case 5: return '很好';
      default: return '未知';
    }
  }

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'hours': hours,
      'quality': quality,
      'date': date.toIso8601String().substring(0, 10),
      'created_at': createdAt.toIso8601String(),
    };
  }

  factory SleepRecord.fromMap(Map<String, dynamic> map) {
    return SleepRecord(
      id: map['id'] as int?,
      hours: (map['hours'] as num).toDouble(),
      quality: map['quality'] as int,
      date: DateTime.parse(map['date'] as String),
      createdAt: DateTime.parse(map['created_at'] as String),
    );
  }
}

3.2 数据库服务封装

// lib/services/database_service.dart
// 数据库服务封装

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as path;
import '../models/health/health_model.dart';

/// 数据库服务类
/// 【核心功能】
/// 1. 数据库初始化和版本管理
/// 2. CRUD操作封装
/// 3. 查询优化
class DatabaseService {
  // 单例模式
  static final DatabaseService _instance = DatabaseService._internal();
  static DatabaseService get instance => _instance;

  DatabaseService._internal();

  Database? _database;

  /// 获取数据库实例(懒加载)
  Future<Database> get database async {
    _database ??= await _initDatabase();
    return _database!;
  }

  /// 初始化数据库
  /// 【踩坑记录】路径拼接要用path包!
  Future<Database> _initDatabase() async {
    // 获取数据库存储路径
    // 这个方法会自动适配不同平台
    final databasesPath = await getDatabasesPath();
    
    // 【关键】用path.join拼接路径,不要用字符串拼接!
    // 否则在某些设备上路径格式不对会报错
    final dbPath = path.join(databasesPath, 'health_app.db');

    // 打开或创建数据库
    return await openDatabase(
      dbPath,
      version: 1,
      onCreate: _onCreate,
      onUpgrade: _onUpgrade,
    );
  }

  /// 创建数据库表
  Future<void> _onCreate(Database db, int version) async {
    // 创建步数记录表
    await db.execute('''
      CREATE TABLE step_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        steps INTEGER NOT NULL,
        date TEXT NOT NULL,
        goal INTEGER DEFAULT 10000,
        created_at TEXT NOT NULL
      )
    ''');

    // 创建喝水记录表
    await db.execute('''
      CREATE TABLE water_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        amount INTEGER NOT NULL,
        date TEXT NOT NULL,
        created_at TEXT NOT NULL
      )
    ''');

    // 创建运动记录表
    await db.execute('''
      CREATE TABLE exercise_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        type TEXT NOT NULL,
        duration_minutes INTEGER NOT NULL,
        calories INTEGER NOT NULL,
        date TEXT NOT NULL,
        created_at TEXT NOT NULL
      )
    ''');

    // 创建睡眠记录表
    await db.execute('''
      CREATE TABLE sleep_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        hours REAL NOT NULL,
        quality INTEGER NOT NULL,
        date TEXT NOT NULL,
        created_at TEXT NOT NULL
      )
    ''');

    // 【性能优化】创建索引
    // 【踩坑记录】按日期查询一定要建索引!
    await db.execute('CREATE INDEX idx_step_date ON step_records(date)');
    await db.execute('CREATE INDEX idx_water_date ON water_records(date)');
    await db.execute('CREATE INDEX idx_exercise_date ON exercise_records(date)');
    await db.execute('CREATE INDEX idx_sleep_date ON sleep_records(date)');
  }

  /// 数据库升级处理
  Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
    // 【示例】版本1到版本2的迁移
    if (oldVersion < 2) {
      // await db.execute('ALTER TABLE ...');
    }
  }

  // ==================== 步数记录 CRUD ====================
  
  /// 获取步数记录列表(最近N天)
  Future<List<StepRecord>> getStepRecords({int limit = 30}) async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query(
      'step_records',
      orderBy: 'date DESC',
      limit: limit,
    );
    return maps.map((map) => StepRecord.fromMap(map)).toList();
  }

  /// 获取今日步数记录
  Future<StepRecord?> getTodayStepRecord() async {
    final db = await database;
    final today = DateTime.now().toIso8601String().substring(0, 10);
    final List<Map<String, dynamic>> maps = await db.query(
      'step_records',
      where: 'date = ?',
      whereArgs: [today],
      limit: 1,
    );
    if (maps.isEmpty) return null;
    return StepRecord.fromMap(maps.first);
  }

  /// 获取指定日期范围的步数
  Future<List<StepRecord>> getStepRecordsByDateRange(
    DateTime start,
    DateTime end,
  ) async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query(
      'step_records',
      where: 'date >= ? AND date <= ?',
      whereArgs: [
        start.toIso8601String().substring(0, 10),
        end.toIso8601String().substring(0, 10),
      ],
      orderBy: 'date ASC',
    );
    return maps.map((map) => StepRecord.fromMap(map)).toList();
  }

  /// 插入步数记录
  /// 【踩坑记录】toMap()里包含id,要移除才能自增
  Future<int> insertStepRecord(StepRecord record) async {
    final db = await database;
    final map = record.toMap()..remove('id');
    return await db.insert('step_records', map);
  }

  /// 更新步数记录
  Future<int> updateStepRecord(StepRecord record) async {
    final db = await database;
    return await db.update(
      'step_records',
      record.toMap(),
      where: 'id = ?',
      whereArgs: [record.id],
    );
  }

  /// 删除步数记录
  Future<int> deleteStepRecord(int id) async {
    final db = await database;
    return await db.delete(
      'step_records',
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  // ==================== 喝水记录 CRUD ====================

  /// 获取喝水记录列表
  Future<List<WaterRecord>> getWaterRecords() async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query(
      'water_records',
      orderBy: 'date DESC, created_at DESC',
    );
    return maps.map((map) => WaterRecord.fromMap(map)).toList();
  }

  /// 获取今日喝水量
  Future<int> getTodayWaterAmount() async {
    final db = await database;
    final today = DateTime.now().toIso8601String().substring(0, 10);
    final result = await db.rawQuery(
      'SELECT SUM(amount) as total FROM water_records WHERE date = ?',
      [today],
    );
    return (result.first['total'] as int?) ?? 0;
  }

  /// 获取指定日期的喝水记录
  Future<List<WaterRecord>> getWaterRecordsByDate(DateTime date) async {
    final db = await database;
    final dateStr = date.toIso8601String().substring(0, 10);
    final List<Map<String, dynamic>> maps = await db.query(
      'water_records',
      where: 'date = ?',
      whereArgs: [dateStr],
      orderBy: 'created_at DESC',
    );
    return maps.map((map) => WaterRecord.fromMap(map)).toList();
  }

  /// 插入喝水记录
  Future<int> insertWaterRecord(WaterRecord record) async {
    final db = await database;
    final map = record.toMap()..remove('id');
    return await db.insert('water_records', map);
  }

  /// 删除喝水记录
  Future<int> deleteWaterRecord(int id) async {
    final db = await database;
    return await db.delete(
      'water_records',
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  // ==================== 运动记录 CRUD ====================

  /// 获取运动记录列表
  Future<List<ExerciseRecord>> getExerciseRecords({int limit = 100}) async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query(
      'exercise_records',
      orderBy: 'date DESC, created_at DESC',
      limit: limit,
    );
    return maps.map((map) => ExerciseRecord.fromMap(map)).toList();
  }

  /// 获取今日运动记录
  Future<List<ExerciseRecord>> getTodayExerciseRecords() async {
    final db = await database;
    final today = DateTime.now().toIso8601String().substring(0, 10);
    final List<Map<String, dynamic>> maps = await db.query(
      'exercise_records',
      where: 'date = ?',
      whereArgs: [today],
      orderBy: 'created_at DESC',
    );
    return maps.map((map) => ExerciseRecord.fromMap(map)).toList();
  }

  /// 获取本周总运动时长
  Future<int> getWeeklyExerciseMinutes() async {
    final db = await database;
    final now = DateTime.now();
    final weekStart = now.subtract(Duration(days: now.weekday - 1));
    final weekStartStr = weekStart.toIso8601String().substring(0, 10);
    
    final result = await db.rawQuery(
      'SELECT SUM(duration_minutes) as total FROM exercise_records WHERE date >= ?',
      [weekStartStr],
    );
    return (result.first['total'] as int?) ?? 0;
  }

  /// 插入运动记录
  Future<int> insertExerciseRecord(ExerciseRecord record) async {
    final db = await database;
    final map = record.toMap()..remove('id');
    return await db.insert('exercise_records', map);
  }

  /// 删除运动记录
  Future<int> deleteExerciseRecord(int id) async {
    final db = await database;
    return await db.delete(
      'exercise_records',
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  // ==================== 睡眠记录 CRUD ====================

  /// 获取睡眠记录列表
  Future<List<SleepRecord>> getSleepRecords({int limit = 30}) async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query(
      'sleep_records',
      orderBy: 'date DESC',
      limit: limit,
    );
    return maps.map((map) => SleepRecord.fromMap(map)).toList();
  }

  /// 获取本周平均睡眠时长
  Future<double> getWeeklySleepAverage() async {
    final db = await database;
    final now = DateTime.now();
    final weekStart = now.subtract(Duration(days: now.weekday - 1));
    final weekStartStr = weekStart.toIso8601String().substring(0, 10);
    
    final result = await db.rawQuery(
      'SELECT AVG(hours) as average FROM sleep_records WHERE date >= ?',
      [weekStartStr],
    );
    return (result.first['average'] as double?) ?? 7.0;
  }

  /// 插入睡眠记录
  Future<int> insertSleepRecord(SleepRecord record) async {
    final db = await database;
    final map = record.toMap()..remove('id');
    return await db.insert('sleep_records', map);
  }

  /// 删除睡眠记录
  Future<int> deleteSleepRecord(int id) async {
    final db = await database;
    return await db.delete(
      'sleep_records',
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  // ==================== 统计查询 ====================

  /// 获取健康数据统计
  Future<Map<String, dynamic>> getHealthStatistics() async {
    final db = await database;
    final now = DateTime.now();
    final todayStr = now.toIso8601String().substring(0, 10);
    final weekStart = now.subtract(Duration(days: now.weekday - 1));
    final weekStartStr = weekStart.toIso8601String().substring(0, 10);

    // 今日步数
    final todaySteps = await db.rawQuery(
      'SELECT SUM(steps) as total FROM step_records WHERE date = ?',
      [todayStr],
    );

    // 今日喝水量
    final todayWater = await db.rawQuery(
      'SELECT SUM(amount) as total FROM water_records WHERE date = ?',
      [todayStr],
    );

    // 本周运动
    final weekExercise = await db.rawQuery(
      'SELECT SUM(duration_minutes) as minutes, SUM(calories) as calories FROM exercise_records WHERE date >= ?',
      [weekStartStr],
    );

    return {
      'today_steps': (todaySteps.first['total'] as int?) ?? 0,
      'today_water': (todayWater.first['total'] as int?) ?? 0,
      'week_exercise_minutes': (weekExercise.first['minutes'] as int?) ?? 0,
      'week_calories': (weekExercise.first['calories'] as int?) ?? 0,
    };
  }

  // ==================== 批量操作 ====================

  /// 批量插入数据(使用事务)
  Future<void> batchInsertStepRecords(List<StepRecord> records) async {
    final db = await database;
    
    await db.transaction((txn) async {
      for (final record in records) {
        final map = record.toMap()..remove('id');
        await txn.insert('step_records', map);
      }
    });
  }

  /// 清空所有数据(谨慎使用!)
  Future<void> clearAllData() async {
    final db = await database;
    await db.transaction((txn) async {
      await txn.delete('step_records');
      await txn.delete('water_records');
      await txn.delete('exercise_records');
      await txn.delete('sleep_records');
    });
  }

  /// 关闭数据库
  Future<void> close() async {
    final db = _database;
    if (db != null) {
      await db.close();
      _database = null;
    }
  }
}

3.3 在BLoC中使用数据库服务

// lib/bloc/health/health_bloc.dart
// 健康模块BLoC(展示如何与数据库结合)

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../models/health/health_model.dart';
import '../../services/database_service.dart';

// 事件定义
abstract class HealthEvent extends Equatable {
  const HealthEvent();
  
  List<Object?> get props => [];
}

class LoadHealthData extends HealthEvent {}
class AddSteps extends HealthEvent {
  final int steps;
  const AddSteps(this.steps);
  
  List<Object?> get props => [steps];
}
class AddWaterIntake extends HealthEvent {
  final int amount;
  const AddWaterIntake(this.amount);
  
  List<Object?> get props => [amount];
}
class AddExerciseRecord extends HealthEvent {
  final ExerciseRecord record;
  const AddExerciseRecord(this.record);
  
  List<Object?> get props => [record];
}
class AddSleepRecord extends HealthEvent {
  final double hours;
  final int quality;
  const AddSleepRecord({required this.hours, required this.quality});
  
  List<Object?> get props => [hours, quality];
}

// 状态定义
enum HealthStatus { initial, loading, loaded, error }

class HealthState extends Equatable {
  final HealthStatus status;
  final int currentSteps;
  final int currentWater;
  final List<ExerciseRecord> exerciseRecords;
  final String? errorMessage;

  const HealthState({
    this.status = HealthStatus.initial,
    this.currentSteps = 0,
    this.currentWater = 0,
    this.exerciseRecords = const [],
    this.errorMessage,
  });

  HealthState copyWith({
    HealthStatus? status,
    int? currentSteps,
    int? currentWater,
    List<ExerciseRecord>? exerciseRecords,
    String? errorMessage,
  }) {
    return HealthState(
      status: status ?? this.status,
      currentSteps: currentSteps ?? this.currentSteps,
      currentWater: currentWater ?? this.currentWater,
      exerciseRecords: exerciseRecords ?? this.exerciseRecords,
      errorMessage: errorMessage,
    );
  }

  
  List<Object?> get props => [status, currentSteps, currentWater, exerciseRecords, errorMessage];
}

// BLoC实现
class HealthBloc extends Bloc<HealthEvent, HealthState> {
  final DatabaseService _databaseService;

  HealthBloc({required DatabaseService databaseService})
      : _databaseService = databaseService,
        super(const HealthState()) {
    on<LoadHealthData>(_onLoadHealthData);
    on<AddSteps>(_onAddSteps);
    on<AddWaterIntake>(_onAddWaterIntake);
    on<AddExerciseRecord>(_onAddExerciseRecord);
    on<AddSleepRecord>(_onAddSleepRecord);
  }

  Future<void> _onLoadHealthData(
    LoadHealthData event,
    Emitter<HealthState> emit,
  ) async {
    emit(state.copyWith(status: HealthStatus.loading));

    try {
      // 【关键】从数据库加载数据
      final todaySteps = await _databaseService.getTodayStepRecord();
      final todayWater = await _databaseService.getTodayWaterAmount();
      final exercises = await _databaseService.getTodayExerciseRecords();

      emit(state.copyWith(
        status: HealthStatus.loaded,
        currentSteps: todaySteps?.steps ?? 0,
        currentWater: todayWater,
        exerciseRecords: exercises,
      ));
    } catch (e) {
      emit(state.copyWith(
        status: HealthStatus.error,
        errorMessage: '加载失败: $e',
      ));
    }
  }

  Future<void> _onAddSteps(
    AddSteps event,
    Emitter<HealthState> emit,
  ) async {
    try {
      final record = StepRecord(
        steps: event.steps,
        date: DateTime.now(),
      );
      // 【关键】存入数据库
      await _databaseService.insertStepRecord(record);
      
      emit(state.copyWith(
        currentSteps: state.currentSteps + event.steps,
      ));
    } catch (e) {
      emit(state.copyWith(errorMessage: '添加步数失败'));
    }
  }

  Future<void> _onAddWaterIntake(
    AddWaterIntake event,
    Emitter<HealthState> emit,
  ) async {
    try {
      final record = WaterRecord(
        amount: event.amount,
        date: DateTime.now(),
      );
      await _databaseService.insertWaterRecord(record);
      
      emit(state.copyWith(
        currentWater: state.currentWater + event.amount,
      ));
    } catch (e) {
      emit(state.copyWith(errorMessage: '添加喝水记录失败'));
    }
  }

  Future<void> _onAddExerciseRecord(
    AddExerciseRecord event,
    Emitter<HealthState> emit,
  ) async {
    try {
      await _databaseService.insertExerciseRecord(event.record);
      
      emit(state.copyWith(
        exerciseRecords: [event.record, ...state.exerciseRecords],
      ));
    } catch (e) {
      emit(state.copyWith(errorMessage: '添加运动记录失败'));
    }
  }

  Future<void> _onAddSleepRecord(
    AddSleepRecord event,
    Emitter<HealthState> emit,
  ) async {
    try {
      final record = SleepRecord(
        hours: event.hours,
        quality: event.quality,
        date: DateTime.now(),
      );
      await _databaseService.insertSleepRecord(record);
    } catch (e) {
      emit(state.copyWith(errorMessage: '添加睡眠记录失败'));
    }
  }
}

四、开发过程中的踩坑与挫折实录 😤

4.1 第一个大坑:数据库路径拼接错误 💥

问题描述

数据库怎么也创建不成功,报错Unable to open database file

排查过程

  1. 检查权限配置——没问题
  2. 检查目录是否存在——存在
  3. 最后发现是路径拼接方式不对

错误代码

// ❌ 错误写法
final dbPath = databasesPath + '/' + 'health_app.db';
// 在某些设备上路径分隔符可能不是'/'

正确写法

// ✅ 正确写法
import 'package:path/path.dart' as path;
final dbPath = path.join(databasesPath, 'health_app.db');
// path.join会自动处理路径分隔符

4.2 第二个大坑:DateTime类型存取出错 🕐

问题描述

存进去的日期,拿出来变成了奇怪的时间!

排查过程

  1. 检查存入的数据——看起来没问题
  2. 检查SQLite存储格式——发现问题了!
  3. 最后发现是DateTime和String转换的问题

错误代码

// ❌ 错误写法
await db.execute('''
  CREATE TABLE test (
    created_at DATETIME  // SQLite没有DATETIME类型!
  )
''');

正确写法

// ✅ 正确写法
await db.execute('''
  CREATE TABLE test (
    created_at TEXT  // 用TEXT存储ISO8601字符串
  )
''');

// Dart侧转换
// 存:date.toIso8601String()
// 取:DateTime.parse(string)

4.3 第三个大坑:查询太慢没建索引 🐢

问题描述

数据一多,按日期查询就特别慢!

排查过程

  1. 检查SQL语句——没问题
  2. 检查数据量——几千条数据
  3. 最后发现是没建索引

解决方案

// 【关键】建索引!
await db.execute('CREATE INDEX idx_step_date ON step_records(date)');
await db.execute('CREATE INDEX idx_water_date ON water_records(date)');
await db.execute('CREATE INDEX idx_exercise_date ON exercise_records(date)');
await db.execute('CREATE INDEX idx_sleep_date ON sleep_records(date)');

五、鸿蒙专属适配方案 🔧

5.1 数据库性能优化配置

Future<Database> _initDatabase() async {
  final databasesPath = await getDatabasesPath();
  final dbPath = path.join(databasesPath, 'health_app.db');

  return await openDatabase(
    dbPath,
    version: 1,
    onCreate: _onCreate,
    // 【性能优化】数据库配置
    onConfigure: (db) async {
      // 启用WAL模式(提高并发性能)
      await db.execute('PRAGMA journal_mode = WAL');
      // 同步模式
      await db.execute('PRAGMA synchronous = NORMAL');
      // 缓存大小(负数表示KB)
      await db.execute('PRAGMA cache_size = -2000');
      // 临时表存储在内存
      await db.execute('PRAGMA temp_store = MEMORY');
    },
  );
}

5.2 数据库迁移策略

Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
  // 【版本迁移】不要删除旧表,要用ALTER TABLE
  if (oldVersion < 2) {
    // 添加新字段
    await db.execute('''
      ALTER TABLE step_records ADD COLUMN calories INTEGER DEFAULT 0
    ''');
  }
  
  if (oldVersion < 3) {
    // 创建新表
    await db.execute('''
      CREATE TABLE IF NOT EXISTS new_table (
        id INTEGER PRIMARY KEY,
        data TEXT
      )
    ''');
    
    // 迁移数据
    await db.execute('''
      INSERT INTO new_table SELECT * FROM old_table
    ''');
    
    // 删除旧表
    await db.execute('DROP TABLE old_table');
  }
}

六、最终实现效果验证 ✅

经过一番踩坑和修复,数据库在鸿蒙设备上完美运行!

实现的功能包括

  • ✅ 数据库初始化与版本管理
  • ✅ 步数/喝水/运动/睡眠记录的CRUD
  • ✅ 按日期范围查询
  • ✅ 统计聚合查询
  • ✅ 批量操作和事务
  • ✅ 索引优化查询性能

(此处附鸿蒙设备上成功运行的截图)
在这里插入图片描述
在这里插入图片描述

截图应该包含:

  1. App重启后数据依然存在
  2. 数据库文件正常创建

七、个人学习总结与心得 🎓

7.1 数据库的学习收获

技术层面

  • 学会了sqflite数据库的使用
  • 学会了CRUD操作的封装
  • 学会了SQL查询优化(索引)
  • 学会了数据库迁移策略

数据库知识

  • 理解了SQLite的数据类型(INTEGER、REAL、TEXT、BLOB)
  • 学会了建索引提升查询性能
  • 理解了事务和批量操作

7.2 踩坑反思

sqflite使用时最容易踩的坑:

  1. 路径拼接——一定要用path包
  2. DateTime存储——转成ISO8601字符串
  3. 索引——按日期查询必须建索引
  4. 版本迁移——不要删表,用ALTER

7.3 后续计划

数据库还有很多可以玩的地方:

  • 📊 数据库加密(sqlcipher)
  • 📱 数据库同步(云端备份)
  • 🧹 数据库清理(自动删除旧数据)
  • 📈 数据库监控(查询性能分析)

结语

好了,sqflite的本地数据库实战就讲到这里!

如果你觉得这篇文章有帮助,欢迎加入我们的开源鸿蒙跨平台社区:

https://openharmonycrossplatform.csdn.net

有问题可以在评论区留言,我会尽量回复!👋

祝大家数据存储稳稳当当!💾✨


往期推荐

  • 「Flutter三方库flutter_bloc的鸿蒙化适配与实战指南」
  • 「Flutter三方库fl_chart的鸿蒙化适配与实战指南」
  • 「Flutter三方库go_router的鸿蒙化适配与实战指南」
  • 「Flutter三方库Provider的鸿蒙化适配与实战指南」

标签:Flutter OpenHarmony sqflite SQLite 数据库 鸿蒙跨平台 数据持久化

首发于:CSDN开源鸿蒙跨平台社区

Logo

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

更多推荐