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}');
}