1. 从零开始的存储之旅

在移动应用开发中,数据存储就像给手机应用建造记忆宫殿。想象你正在开发一款健身打卡应用:需要记住用户的体重变化轨迹、保存训练计划模板、缓存用户偏好的主题设置。这些场景都需要可靠的数据存储方案。

Dart语言作为Flutter框架的官方语言,提供了多种存储解决方案。就像装修房子要选对工具,我们会根据数据类型选择存储方式:简单配置用保险柜(shared_preferences),结构化数据用档案室(SQLite),复杂对象用定制储物柜(对象存储)。让我们通过具体案例,看看如何用Dart建造这些"记忆宫殿"。

2. 基础存储:SharedPreferences实战

2.1 技术选型说明

技术栈:shared_preferences + path_provider
适用场景:用户设置、简单键值对、小型数据缓存

import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart';

// 初始化存储实例
Future<SharedPreferences> initStorage() async {
  // 获取应用文档目录(关联技术说明)
  final directory = await getApplicationDocumentsDirectory();
  print('存储路径:${directory.path}'); // 输出:/data/user/0/com.example.app/app_flutter
  
  // 创建SharedPreferences实例
  return await SharedPreferences.getInstance();
}

// 用户偏好设置示例
void handleUserPreferences() async {
  final prefs = await initStorage();
  
  // 写入数据三部曲
  await prefs.setBool('dark_mode', true);         // 布尔值存储
  await prefs.setDouble('target_weight', 65.5);  // 浮点数存储
  await prefs.setStringList('workout_days',      // 列表存储
    ['Mon', 'Wed', 'Fri']);
  
  // 读取数据示范
  final isDarkMode = prefs.getBool('dark_mode') ?? false;
  final targetWeight = prefs.getDouble('target_weight') ?? 60.0;
  final workoutDays = prefs.getStringList('workout_days') ?? [];
  
  // 删除指定数据
  if (targetWeight < 50) {
    await prefs.remove('target_weight');
  }
}

2.2 关键技术解析

  • path_provider:获取沙盒存储路径,保证不同平台的路径兼容性
  • 异步处理:所有操作都需await,避免UI线程阻塞
  • 类型安全:严格匹配存取数据类型,setString不能读取为double
  • 数据加密:敏感数据建议配合flutter_secure_storage使用

3. 结构化存储:SQLite深度实践

3.1 技术选型说明

技术栈:sqflite + path_provider
适用场景:健身记录、用户历史数据、复杂查询需求

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

// 健身记录数据模型
class WorkoutRecord {
  final int? id;
  final DateTime date;
  final String exercise;
  final double calories;

  WorkoutRecord({this.id, required this.date, 
                required this.exercise, required this.calories});
}

// 数据库管理类
class DatabaseHelper {
  static final _dbName = 'fitness.db';
  static final _dbVersion = 2;  // 版本更新示例
  
  // 单例模式初始化
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
  
  static Database? _database;
  Future<Database> get database async {
    return _database ??= await _initDatabase();
  }

  // 初始化数据库
  Future<Database> _initDatabase() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, _dbName);
    
    return openDatabase(
      path,
      version: _dbVersion,
      onCreate: _onCreate,
      onUpgrade: _onUpgrade,
    );
  }

  // 创建表结构
  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE workouts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        date TEXT NOT NULL,
        exercise TEXT NOT NULL,
        calories REAL NOT NULL
      )
    ''');
    
    // 创建索引提升查询效率
    await db.execute(
      'CREATE INDEX idx_exercise ON workouts (exercise)');
  }

  // 数据库升级处理
  Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
    if (oldVersion < 2) {
      await db.execute('ALTER TABLE workouts ADD COLUMN duration INTEGER');
    }
  }

  // CRUD操作示例
  Future<int> insertRecord(WorkoutRecord record) async {
    final db = await database;
    return await db.insert('workouts', _toMap(record));
  }

  Future<List<WorkoutRecord>> queryRecords(String exercise) async {
    final db = await database;
    final maps = await db.query(
      'workouts',
      where: 'exercise = ?',
      whereArgs: [exercise],
      orderBy: 'date DESC'
    );
    
    return maps.map((map) => _fromMap(map)).toList();
  }

  // 数据转换方法
  Map<String, dynamic> _toMap(WorkoutRecord record) {
    return {
      'date': record.date.toIso8601String(),
      'exercise': record.exercise,
      'calories': record.calories
    };
  }

  WorkoutRecord _fromMap(Map<String, dynamic> map) {
    return WorkoutRecord(
      id: map['id'],
      date: DateTime.parse(map['date']),
      exercise: map['exercise'],
      calories: map['calories']
    );
  }
}

3.2 关联技术实践

// 数据库事务处理示例
Future<void> batchInsert(List<WorkoutRecord> records) async {
  final db = await DatabaseHelper.instance.database;
  await db.transaction((txn) async {
    for (var record in records) {
      await txn.insert('workouts', DatabaseHelper._toMap(record));
    }
  });
}

// 复杂查询示例
Future<double> calculateTotalCalories() async {
  final db = await DatabaseHelper.instance.database;
  final result = await db.rawQuery(
    'SELECT SUM(calories) AS total FROM workouts'
  );
  return result.first['total'] as double? ?? 0.0;
}

4. 技术方案对比分析

4.1 应用场景矩阵

存储类型 典型场景 数据特征
SharedPrefs 用户设置、临时缓存 <100KB,无关联数据
SQLite 训练记录、用户历史 结构化数据,需要查询
文件存储 图片缓存、日志文件 非结构化大数据
云端同步 多设备数据同步 需要网络交互的数据

4.2 优缺点对比

SharedPreferences优势

  • 零配置快速上手
  • 原生支持基础数据类型
  • 自动处理线程安全

局限性

  • 不适合存储复杂关系数据
  • 没有内置加密机制
  • 大数据存取性能差

SQLite优势

  • 支持复杂SQL查询
  • 事务处理保证数据完整性
  • 可扩展的数据库架构

挑战点

  • 需要手动处理数据迁移
  • 较复杂的数据模型转换
  • 索引优化需要专业知识

5. 开发注意事项

5.1 性能优化要点

  • 批量操作:使用事务处理批量写入
await db.transaction((txn) async {
  for (var i = 0; i < 1000; i++) {
    await txn.insert('table', data);
  }
});
  • 懒加载机制:数据库连接延迟初始化
  • 分页查询:避免一次性加载全部数据
await db.query('table', limit: 20, offset: pageIndex * 20);

5.2 安全防护方案

  • 敏感字段加密存储
String encrypt(String data) {
  // 使用flutter_secure_storage实现
}
  • 定期数据库备份
  • 使用参数化查询防止SQL注入
// 正确做法
await db.query('table', where: 'name = ?', whereArgs: [userInput]);

// 危险做法
await db.rawQuery('SELECT * FROM table WHERE name = "$userInput"');

6. 技术演进方向

6.1 新兴存储方案

  • Isar数据库:基于Dart的NoSQL方案
@Collection()
class Exercise {
  Id? id;
  late String name;
  late double metValue;
}
  • Hive存储:高性能键值对存储
  • Moor:类型安全的SQLite封装

6.2 架构设计建议

  • 实现存储抽象层
abstract class StorageService {
  Future<void> saveUserSettings(UserSettings settings);
  Future<List<WorkoutRecord>> getWorkoutHistory();
}
  • 隔离平台相关代码
  • 编写单元测试覆盖存储逻辑

7. 实战经验总结

在健身应用开发中,我们采用混合存储策略:用户基础设置使用SharedPreferences实现快速存取,训练记录采用SQLite保证查询效率。需要注意版本升级时的数据迁移问题,比如当新增训练时长字段时,通过数据库版本控制实现平滑升级。

关键收获:

  • 根据数据特征选择存储方案
  • 提前设计好数据模型扩展性
  • 重要操作添加异常处理
try {
  await db.insert('table', data);
} on DatabaseException catch (e) {
  logger.error('插入失败:${e.message}');
}