开篇:当应用重启后一切归零时
当你的 Flutter 应用第一次启动时还要重新下载全量配置、用户编辑的草稿在杀进程后消失、离线模式下什么都查不到时,你缺的正是正确的本地持久化设计。
- 用户偏好设置每次启动都要重新拉取
- 列表数据在无网络时变成一片空白
- 草稿、缓存、收藏夹在重启后全部丢失
- 数据量超过 10 万条时列表滚动开始卡顿
Flutter 生态为本地持久化提供了从轻到重的一整条光谱:shared_preferences 适合 KV 小数据,sqflite 提供标准 SQLite 能力,drift 用类型安全和响应式把 SQLite 体验拉满,Isar/Hive 则以对象存储与超高性能著称。本文将系统对比这些方案,带你把"存什么、存哪、怎么迁、怎么同步"全部想清楚。
一、本地存储方案全景对比
1.1 shared_preferences 的适用边界
shared_preferences 本质是平台偏好文件(Android 的 SharedPreferences / iOS 的 NSUserDefaults),适合保存小体量、低频改动的键值:
import 'package:shared_preferences/shared_preferences.dart';
// 写入
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
await prefs.setInt('theme_mode', 1);
await prefs.setBool('first_launch', false);
// 读取
final token = prefs.getString('auth_token');
final theme = prefs.getInt('theme_mode') ?? 0;
| 适用 | 不适用 |
|---|---|
| 开关、主题、语言、token | 大量结构化数据 |
| 启动配置、埋点标识 | 需要 SQL 查询 |
| < 100 个键的小 KV | 需要事务与索引 |
一句话总结:
shared_preferences是"启动快照",不是数据库;塞进业务列表数据是常见反模式。
1.2 主力方案对比表
| 方案 | 类型 | 存储引擎 | 性能 | 学习成本 | 代码生成 |
|---|---|---|---|---|---|
| sqflite | SQL | SQLite | 中 | 中 | 无 |
| drift | SQL 类型安全 | SQLite | 中高 | 中高 | 有 |
| Isar | 对象存储 | 自研 | 极高 | 低 | 有 |
| Hive | KV 对象 | 自研 | 高 | 低 | 有 |
1.3 选型决策树
数据规模小、结构简单、偏好类? → shared_preferences
数据需关系查询、团队熟悉 SQL? → sqflite(简单) / drift(类型安全)
数据以对象为主、追求极速读写? → Isar / Hive
一句话总结:选型先回答三个问题——数据是否强关系、是否需要响应式更新、性能敏感度有多高。
二、sqflite 基础与事务
2.1 打开数据库与建表
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
Future<Database> openDb() async {
final path = await getDatabasesPath();
return openDatabase(
join(path, 'app.db'),
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE todos(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER DEFAULT 0,
created_at INTEGER
)
''');
},
);
}
2.2 CRUD 与查询
// 插入
await db.insert('todos', {'title': '写文章', 'done': 0});
// 查询
final rows = await db.query('todos',
where: 'done = ?', whereArgs: [0], orderBy: 'created_at DESC');
// 更新与删除
await db.update('todos', {'done': 1}, where: 'id = ?', whereArgs: [1]);
await db.delete('todos', where: 'id = ?', whereArgs: [2]);
2.3 事务与并发
// 批量写入务必用事务,掉电/异常可回滚
await db.transaction((txn) async {
await txn.insert('todos', {'title': 'A', 'done': 0});
await txn.insert('todos', {'title': 'B', 'done': 0});
// 抛异常会整体回滚
});
// 并发注意:sqflite 底层是单连接,写操作串行
一句话总结:sqflite 就是标准 SQLite 的 Dart 封装,SQL 熟练度直接决定用它的效率。
三、drift 类型安全与代码生成
3.1 表定义与代码生成
drift 用 Dart 定义表结构,自动生成类型安全 API:
// part 指令启用代码生成
part 'app_database.g.dart';
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 200)();
BoolColumn get done => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase(super.e);
}
# 运行代码生成
dart run build_runner build --delete-conflicting-outputs
3.2 DAO 与查询
@DriftAccessor(tables: [Todos])
class TodoDao extends DatabaseAccessor<AppDatabase> {
TodoDao(super.db);
Future<List<Todo>> pendingTodos() {
final q = select(todos)..where((t) => t.done.equals(false));
return q.orderBy([(t) => OrderingTerm.desc(t.createdAt)]).get();
}
Future<void> markDone(int id) =>
(update(todos)..where((t) => t.id.equals(id))).write(TodosCompanion(done: const Value(true)));
}
3.3 流式查询响应式
drift 最大的差异化优势是查询即流(Stream):
// 表数据一变,Stream 自动发射新结果
Stream<List<Todo>> watchPending() {
return (select(todos)..where((t) => t.done.equals(false)))
.watch();
}
// UI 侧直接响应式绑定
StreamBuilder<List<Todo>>(
stream: dao.watchPending(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, i) => ListTile(title: Text(snapshot.data![i].title)),
);
},
);
一句话总结:drift 让"数据库变化"变成响应式数据流,与
StreamBuilder/状态管理天然衔接,是中型及以上应用的高性价比选择。
四、Isar 性能与对象存储
4.1 Isar 概念与索引
Isar 是面向 Flutter 的高性能对象数据库,直接用 Dart 对象建表:
import 'package:isar/isar.dart';
part 'models.g.dart';
@collection
class Todo {
Id id = Isar.autoIncrement;
late String title;
@Index()
late bool done;
@Index(type: IndexType.value)
late DateTime createdAt;
}
// 打开
final isar = await Isar.open([TodoSchema],
directory: (await getApplicationDocumentsDirectory()).path);
4.2 高性能查询
// 链式查询
final pending = await isar.todos
.filter()
.doneEqualTo(false)
.createdAtGreaterThan(DateTime.now().subtract(const Duration(days: 7)))
.sortByCreatedAtDesc()
.findAll();
// 响应式 watch
final stream = isar.todos
.filter()
.doneEqualTo(false)
.watch(fireImmediately: true);
4.3 Hive:轻量 KV 对象存储
Hive 无依赖、纯 Dart,适合缓存层:
import 'package:hive_flutter/hive_flutter.dart';
// 初始化
await Hive.initFlutter();
final box = await Hive.openBox('cache');
// 读写任意可序列化对象
box.put('user_profile', {'name': 'Alice', 'age': 30});
final profile = box.get('user_profile');
一句话总结:Isar 用"对象即表"消灭 SQL 心智负担,索引与响应式 watch 一应俱全;Hive 则是最轻量的 KV 缓存工具。
五、数据模型演进与迁移
5.1 schema 版本管理
无论 sqflite 还是 drift,都要把版本号与迁移逻辑写在显眼处:
// sqflite 版本化
final db = await openDatabase(path,
version: 2, // 升级版本号
onUpgrade: (db, oldV, newV) async {
if (oldV < 2) {
await db.execute('ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0');
}
});
5.2 drift 的迁移策略
final migration = MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: (m, from, to) async {
if (from < 2) await m.addColumn(todos, todos.priority);
});
| 变更类型 | 是否需迁移 | 示例 |
|---|---|---|
| 加字段(带默认值) | 轻迁移 | ALTER TABLE ... ADD COLUMN |
| 改字段类型 | 重迁移 | 建新表 + 拷贝数据 |
| 删字段 | 重迁移 | 重建表结构 |
| 加索引 | 无需数据迁移 | CREATE INDEX |
5.3 数据备份与导入导出
// sqflite 整库导出
final path = join(await getDatabasesPath(), 'app.db');
final bytes = await File(path).readAsBytes();
// 上传到云存储或写入 iCloud/文档目录
// 恢复
await File(restorePath).writeAsBytes(bytes);
一句话总结:迁移是数据库应用逃不掉的功课——加字段要轻快、改结构要谨慎,备份先行。
六、同步与缓存策略、选型建议
6.1 本地缓存策略
常用模式:网络优先(stale-while-revalidate)或缓存优先(offline-first):
Future<List<Todo>> fetchTodos(CacheManager cache) async {
// 先读本地缓存,立即渲染
final cached = await cache.readTodos();
if (cached.isNotEmpty) {
unawaited(refreshRemote(cache)); // 后台刷新
return cached;
}
return refreshRemote(cache);
}
Future<List<Todo>> refreshRemote(CacheManager cache) async {
final remote = await api.fetchTodos();
await cache.writeTodos(remote);
return remote;
}
6.2 离线同步与冲突
- 记录每行的
updated_at时间戳,同步时按时间戳合并 - 用
dirty标记未上行的本地改动,网络恢复后重放 - 冲突策略:last-write-wins 最简单,字段级合并最精确
6.3 最终选型建议
| 项目规模 | 推荐 | 理由 |
|---|---|---|
| 工具类小应用 | shared_preferences + Hive | 零依赖、启动快 |
| 业务型中型应用 | drift | 类型安全 + 响应式 |
| 数据密集大型应用 | Isar | 性能与查询能力兼备 |
| 已有 SQL 后端团队 | sqflite | SQL 心智复用 |
一句话总结:没有最好的数据库,只有最合适的组合——偏好用 KV、核心数据用 SQL/对象库,缓存与同步策略比选哪个库更重要。
FAQ
常见问题:数据量不大,选 shared_preferences 还是数据库?
答:不超过上百个键、无查询需求时用 shared_preferences;一旦出现"按条件筛选、排序、分页"这类需求,就该上数据库(sqflite/drift/Isar),KV 存储硬撑会把性能与维护双双拉垮。
常见问题:drift 和 sqflite 怎么选?
答:两者底层都是 SQLite。想要编译期检查、自动生成的 CRUD、查询即响应式流,选 drift;想要最小依赖、完全手写 SQL、团队已熟练 sqflite,选 sqflite。
常见问题:数据迁移失败会导致数据丢失吗?
答:迁移代码必须在事务中执行,失败即回滚。上线前先在测试机用生产同构数据演练 onUpgrade,并做好整库备份;drift 还支持对迁移进行单元测试。
常见问题:数据库操作放在主 Isolate 还是后台?
答:轻量读写放主 Isolate 没问题;大批量写入、复杂查询、导入导出务必放到后台 Isolate(compute/Isolate.run),避免掉帧。drift 与 Isar 都提供异步 API 供后台调用。
常见问题:Web 端能使用这些本地存储方案吗?
答:shared_preferences 与 Hive 支持 Web;sqflite/drift 需要配合 WASM 版 SQLite(如 drift 的 web 实现)或改用浏览器 IndexedDB 适配;Isar 对 Web 支持有限,选型时要考虑目标平台。
相关阅读
- Flutter 异步与网络 — 网络数据与本地缓存的衔接
- Flutter 状态管理 — 持久化数据如何进入 UI 状态
- Flutter 流与响应式编程 — drift 流式查询与 Stream 的结合
- Flutter 性能优化 — 大数据量列表与读写性能
- Dart 基础语法 — 异步模型与集合操作
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。