《本地数据库持久化:sqflite、drift 与对象存储》

Flutter 本地持久化全攻略:shared_preferences、sqflite、drift、Isar 与 Hive 方案对比,事务、代码生成、索引查询、数据迁移与同步缓存策略与选型建议。

开篇:当应用重启后一切归零时

当你的 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 主力方案对比表

方案类型存储引擎性能学习成本代码生成
sqfliteSQLSQLite中中无
driftSQL 类型安全SQLite中高中高有
Isar对象存储自研极高低有
HiveKV 对象自研高低有

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 后端团队sqfliteSQL 心智复用

一句话总结:没有最好的数据库,只有最合适的组合——偏好用 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」更多文章

  1. 《响应式与自适应布局:从手机到桌面》
  2. 《深度链接与路由进阶:从内部导航到跨端唤起》
  3. 《Isolate 与并发:从 compute 到 Isolate 组》