Flutter本地数据持久化—SharedPreferences、Hive与SQLite对比|新宇宙博客
返回列表本地数据持久化 深度对比Flutter三大本地存储方案的适用场景、性能特征、数据迁移策略及安全加密最佳实践
本地数据持久化 (Local Storage)
模块:七 - 网络与数据持久化
预计阅读时间:28 分钟
前言
"离线优先(Offline First)"是现代移动应用的重要设计原则。用户期望 App 在无网络时也能正常使用,数据在重启后依然保留。实现这一目标,需要在多种本地存储方案中做出正确选择。
Flutter 生态中的本地存储方案可以分为三层:
键值存储 :SharedPreferences、flutter_secure_storage
关系型数据库 :SQLite(sqflite/drift)
NoSQL/对象数据库 :Hive、Isar
本文深入每种方案的实现原理、性能特点和适用场景,并给出生产级的使用范式。
一、SharedPreferences:简单键值存储
1.1 原理与限制
SharedPreferences 在 Android 上对应同名 API(XML 文件),在 iOS 上对应 NSUserDefaults,在 Web 上对应 localStorage。
核心特点 :
存储小量基础类型数据(String、int、double、bool、List<String>)
数据以 XML/Plist 格式持久化到磁盘
全量加载到内存(第一次读取时)
不适合 存储大量数据或复杂对象
dependencies:
shared_preferences: ^2.2.0
1.2 规范化封装
直接使用 SharedPreferences 的字符串 key 容易出错。推荐封装成类型安全的存取器:
class AppPreferences {
static const _kThemeMode = 'theme_mode';
static const _kLanguage = 'language';
static const _kOnboardingComplete = 'onboarding_complete';
static const _kUserId = 'user_id';
static const _kLastSyncTime = 'last_sync_time';
final SharedPreferences _prefs;
AppPreferences._(this._prefs);
// 单例模式
static AppPreferences? _instance;
static Future<AppPreferences> getInstance() async {
if (_instance == null) {
final prefs = await SharedPreferences.getInstance();
_instance = AppPreferences._(prefs);
}
return _instance!;
}
// 主题模式
ThemeMode get themeMode {
final value = _prefs.getString(_kThemeMode);
return ThemeMode.values.firstWhere(
(e) => e.name == value,
orElse: () => ThemeMode.system,
);
}
Future<void> setThemeMode(ThemeMode mode) =>
_prefs.setString(_kThemeMode, mode.name);
// 语言
String get language => _prefs.getString(_kLanguage) ?? 'zh';
Future<void> setLanguage(String code) => _prefs.setString(_kLanguage, code);
// 是否完成引导
bool get isOnboardingComplete =>
_prefs.getBool(_kOnboardingComplete) ?? false;
Future<void> completeOnboarding() =>
_prefs.setBool(_kOnboardingComplete, true);
// 用户 ID
int? get userId => _prefs.getInt(_kUserId);
Future<void> setUserId(int id) => _prefs.setInt(_kUserId, id);
Future<void> clearUserId() => _prefs.remove(_kUserId);
// 上次同步时间
DateTime? get lastSyncTime {
final ms = _prefs.getInt(_kLastSyncTime);
return ms != null ? DateTime.fromMillisecondsSinceEpoch(ms) : null;
}
Future<void> setLastSyncTime(DateTime time) =>
_prefs.setInt(_kLastSyncTime, time.millisecondsSinceEpoch);
// 清除所有数据(退出登录时调用)
Future<void> clearAll() => _prefs.clear();
}
二、flutter_secure_storage:加密存储
2.1 为什么需要加密存储 SharedPreferences 的数据是明文存储的,在 root 的 Android 设备或越狱的 iOS 设备上可以直接读取。对于 Token、密码等敏感数据,需要使用加密存储。
Android :使用 Android Keystore + AES256 加密
iOS :使用 Keychain Services
dependencies:
flutter_secure_storage: ^9.0.0
class TokenStorage {
static const _kAccessToken = 'access_token';
static const _kRefreshToken = 'refresh_token';
static const _kTokenExpiry = 'token_expiry';
final FlutterSecureStorage _storage;
TokenStorage({
FlutterSecureStorage? storage,
}) : _storage = storage ?? const FlutterSecureStorage(
// Android 加密选项
aOptions: AndroidOptions(
encryptedSharedPreferences: true, // 使用 EncryptedSharedPreferences
),
// iOS Keychain 选项
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock, // 首次解锁后可访问
),
);
Future<String?> getAccessToken() => _storage.read(key: _kAccessToken);
Future<String?> getRefreshToken() => _storage.read(key: _kRefreshToken);
Future<void> saveTokens({
required String accessToken,
required String refreshToken,
required DateTime expiry,
}) async {
await Future.wait([
_storage.write(key: _kAccessToken, value: accessToken),
_storage.write(key: _kRefreshToken, value: refreshToken),
_storage.write(
key: _kTokenExpiry,
value: expiry.toIso8601String(),
),
]);
}
Future<bool> isTokenExpired() async {
final expiryStr = await _storage.read(key: _kTokenExpiry);
if (expiryStr == null) return true;
final expiry = DateTime.parse(expiryStr);
// 提前 30 秒认为过期,留出刷新时间
return DateTime.now().isAfter(expiry.subtract(const Duration(seconds: 30)));
}
Future<void> clearTokens() async {
await Future.wait([
_storage.delete(key: _kAccessToken),
_storage.delete(key: _kRefreshToken),
_storage.delete(key: _kTokenExpiry),
]);
}
}
三、SQLite(drift):关系型存储
3.1 为什么选 drift 而不是直接用 sqflite sqflite 是 SQLite 的底层封装,直接操作原始 SQL。drift(原 moor)在其上提供了:
类型安全 :Dart DSL 写查询,编译期检查
代码生成 :自动生成 DAO、查询接口
响应式查询 :返回 Stream<List<T>>,数据变化自动通知
数据库迁移 :版本化的迁移脚本
dependencies:
drift: ^2.14.0
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.1.0
path: ^1.8.0
dev_dependencies:
drift_dev: ^2.14.0
build_runner: ^2.4.0
3.2 定义数据表 import 'package:drift/drift.dart';
// 定义 todos 表
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 500)();
TextColumn get content => text().nullable()();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().nullable()();
IntColumn get categoryId => integer().nullable().references(Categories, #id)();
}
// 定义 categories 表
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get color => text().withDefault(const Constant('#2196F3'))();
}
3.3 定义数据库 part 'database.g.dart';
@DriftDatabase(tables: [Todos, Categories])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 2;
// 数据库迁移策略
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (Migrator m) async {
// 首次创建数据库
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) {
// v1 → v2:给 todos 表添加 updated_at 字段
await m.addColumn(todos, todos.updatedAt);
}
// if (from < 3) { ... } 继续添加
},
beforeOpen: (details) async {
// 开启外键约束(SQLite 默认关闭)
await customStatement('PRAGMA foreign_keys = ON');
},
);
}
}
// 数据库文件路径(在 Isolate 中安全使用)
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(path.join(dbFolder.path, 'app.db'));
return NativeDatabase(file);
});
}
3.4 DAO(数据访问对象) part of 'database.dart'; // 或者单独文件
// 在数据库类中定义 DAO:
@DriftDatabase(tables: [Todos, Categories], daos: [TodoDao])
class AppDatabase extends _$AppDatabase { ... }
// DAO 定义
@DaoScope()
class TodoDao extends DatabaseAccessor<AppDatabase> with _$TodoDaoMixin {
TodoDao(super.db);
// ─── 基础 CRUD ───────────────────────────────────────────────────────────
// 响应式查询:数据变化时自动推送新列表
Stream<List<Todo>> watchAllTodos() => select(todos).watch();
// 按状态查询
Stream<List<Todo>> watchTodosByStatus({required bool completed}) {
return (select(todos)..where((t) => t.completed.equals(completed))).watch();
}
// 带分页的查询
Future<List<Todo>> getTodosPaginated({
required int page,
required int limit,
}) {
return (select(todos)
..limit(limit, offset: page * limit)
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.get();
}
// 插入(返回新记录的 ID)
Future<int> insertTodo(TodosCompanion todo) => into(todos).insert(todo);
// 更新
Future<bool> updateTodo(Todo todo) => update(todos).replace(todo);
// 通过 Companion 更新(只更新指定字段)
Future<int> updateTodoTitle(int id, String newTitle) {
return (update(todos)..where((t) => t.id.equals(id)))
.write(TodosCompanion(
title: Value(newTitle),
updatedAt: Value(DateTime.now()),
));
}
// 删除
Future<int> deleteTodo(int id) =>
(delete(todos)..where((t) => t.id.equals(id))).go();
// ─── 复杂查询 ───────────────────────────────────────────────────────────
// JOIN 查询(带分类信息)
Stream<List<TodoWithCategory>> watchTodosWithCategory() {
final query = select(todos).join([
leftOuterJoin(categories, categories.id.equalsExp(todos.categoryId)),
]);
return query.watch().map((rows) {
return rows.map((row) {
return TodoWithCategory(
todo: row.readTable(todos),
category: row.readTableOrNull(categories),
);
}).toList();
});
}
// 聚合查询
Future<int> getCompletedCount() async {
final count = todos.id.count();
final query = selectOnly(todos)
..addColumns([count])
..where(todos.completed.equals(true));
final result = await query.getSingle();
return result.read(count) ?? 0;
}
// 全文搜索(模糊匹配)
Future<List<Todo>> searchTodos(String keyword) {
return (select(todos)
..where((t) =>
t.title.contains(keyword) | t.content.contains(keyword)))
.get();
}
// 批量操作(事务)
Future<void> markAllComplete() {
return transaction(() async {
await (update(todos)).write(
const TodosCompanion(completed: Value(true)),
);
});
}
// 复杂事务
Future<void> deleteCompletedAndLog() {
return transaction(() async {
final completed = await (select(todos)
..where((t) => t.completed.equals(true)))
.get();
await (delete(todos)..where((t) => t.completed.equals(true))).go();
// 在同一事务内记录日志...
debugPrint('Deleted ${completed.length} completed todos');
});
}
}
// 自定义结果类型
class TodoWithCategory {
const TodoWithCategory({required this.todo, this.category});
final Todo todo;
final Category? category;
}
四、Hive vs Isar:NoSQL 方案
4.1 Hive:轻量级键值 NoSQL Hive 是纯 Dart 实现的 NoSQL 数据库,无需 SQLite 依赖:
dependencies:
hive_flutter: ^1.1.0
dev_dependencies:
hive_generator: ^2.0.0
build_runner: ^2.4.0
part 'product.g.dart';
@HiveType(typeId: 1) // typeId 必须全局唯一
class Product extends HiveObject {
@HiveField(0)
late String id;
@HiveField(1)
late String name;
@HiveField(2)
late double price;
@HiveField(3)
late DateTime updatedAt;
}
// 初始化
Future<void> initHive() async {
await Hive.initFlutter();
Hive.registerAdapter(ProductAdapter()); // 注册生成的 Adapter
await Hive.openBox<Product>('products');
}
// 使用
class ProductCache {
final Box<Product> _box = Hive.box<Product>('products');
void save(Product product) => _box.put(product.id, product);
Product? get(String id) => _box.get(id);
List<Product> getAll() => _box.values.toList();
// 响应式监听(watchObject/watch)
Stream<BoxEvent> watchAll() => _box.watch();
Future<void> delete(String id) => _box.delete(id);
Future<void> clear() => _box.clear();
}
4.2 Isar:高性能 NoSQL(推荐替代 Hive) Isar 是 Hive 作者的新作,性能大幅超越 Hive,并支持复杂查询:
dependencies:
isar: ^3.1.0
isar_flutter_libs: ^3.1.0
path_provider: ^2.1.0
dev_dependencies:
isar_generator: ^3.1.0
build_runner: ^2.4.0
part 'product.g.dart';
@collection
class Product {
Id id = Isar.autoIncrement; // 自增 ID
@Index(unique: true)
late String sku;
late String name;
@Index() // 为经常查询的字段加索引
late String category;
late double price;
@Index()
late DateTime createdAt;
// 嵌套对象(非关系型,序列化存储)
late List<String> tags;
}
// 初始化 Isar
Future<Isar> openIsar() async {
final dir = await getApplicationDocumentsDirectory();
return Isar.open(
[ProductSchema],
directory: dir.path,
);
}
// 使用 Isar
class IsarProductRepository {
final Isar _isar;
IsarProductRepository(this._isar);
// 基础增删改查
Future<void> saveProduct(Product product) async {
await _isar.writeTxn(() async {
await _isar.products.put(product);
});
}
Future<Product?> getProductBySku(String sku) {
return _isar.products.getBySku(sku);
}
// 复杂查询(类型安全的 Isar DSL)
Future<List<Product>> searchProducts({
String? keyword,
String? category,
double? maxPrice,
int limit = 20,
int offset = 0,
}) {
var query = _isar.products.where();
if (category != null) {
query = query.categoryEqualTo(category);
}
return query
.filter()
.optional(keyword != null, (q) => q.nameContains(keyword!))
.optional(maxPrice != null, (q) => q.priceLessThan(maxPrice!))
.sortByCreatedAtDesc()
.offset(offset)
.limit(limit)
.findAll();
}
// 响应式查询(Stream)
Stream<List<Product>> watchProductsByCategory(String category) {
return _isar.products
.where()
.categoryEqualTo(category)
.watch(fireImmediately: true);
}
// 批量操作
Future<void> saveProducts(List<Product> products) async {
await _isar.writeTxn(() async {
await _isar.products.putAll(products);
});
}
Future<int> getCount() => _isar.products.count();
}
4.3 Hive vs Isar 对比 维度 Hive Isar 查询能力 仅 key 查询,过滤需手动 完整 DSL 查询,支持索引 性能 好 极好(C 编写的内核) 包大小 小(纯 Dart) 稍大(需要原生库) 事务支持 基础 完整 ACID 事务 全文搜索 ❌ ✅(内置) 关系 ❌ ✅(Links) 适用场景 简单对象缓存 中复杂度查询需求
五、离线优先架构:缓存 + 同步策略
5.1 Repository 模式中的数据层决策 class ProductRepository {
final ProductApi _api; // 网络数据源
final IsarProductRepository _cache; // 本地缓存
ProductRepository({
required ProductApi api,
required IsarProductRepository cache,
}) : _api = api, _cache = cache;
// 策略一:Cache-First(离线优先)
// 先返回缓存,同时后台更新
Stream<List<Product>> getProductsStream() async* {
// 1. 立即返回缓存数据
final cached = await _cache.getAll();
if (cached.isNotEmpty) yield cached;
// 2. 后台拉取新数据
try {
final fresh = await _api.getProducts();
await _cache.saveProducts(fresh);
yield fresh; // 3. 推送新数据
} on NetworkException {
// 网络失败:已经推送了缓存,不需要处理
if (cached.isEmpty) rethrow; // 如果缓存也没有,才抛出错误
}
}
// 策略二:Network-First(实时优先)
// 优先网络,失败时用缓存
Future<List<Product>> getProducts() async {
try {
final products = await _api.getProducts();
await _cache.saveProducts(products); // 更新缓存
return products;
} on NetworkException {
final cached = await _cache.getAll();
if (cached.isNotEmpty) return cached;
rethrow;
}
}
// 策略三:Stale-While-Revalidate(带过期的缓存)
Future<List<Product>> getProductsSWR() async {
final lastSync = await _cache.getLastSyncTime();
final isFresh = lastSync != null &&
DateTime.now().difference(lastSync) < const Duration(minutes: 5);
if (isFresh) {
// 缓存足够新鲜,直接返回
return _cache.getAll();
}
// 缓存过期,重新拉取
final products = await _api.getProducts();
await _cache.saveProducts(products);
await _cache.setLastSyncTime(DateTime.now());
return products;
}
}
5.2 乐观更新(Optimistic Update) // 本地先更新,然后同步到服务端;失败时回滚
Future<void> toggleTodoComplete(int todoId) async {
// 1. 获取当前状态
final todo = await _db.todoDao.getTodoById(todoId);
final newCompleted = !todo.completed;
// 2. 乐观更新本地状态
await _db.todoDao.updateTodo(
todo.copyWith(completed: newCompleted, updatedAt: Value(DateTime.now())),
);
// 3. 同步到服务端
try {
await _api.updateTodo(todoId, completed: newCompleted);
} catch (e) {
// 4. 失败时回滚本地状态
await _db.todoDao.updateTodo(
todo.copyWith(completed: !newCompleted),
);
rethrow;
}
}
六、数据库加密
6.1 drift 加密 dependencies:
drift: ^2.14.0
sqlcipher_flutter_libs: ^0.6.0
dev_dependencies:
drift_dev: ^2.14.0
import 'package:drift/native.dart';
import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
LazyDatabase _openEncryptedConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(path.join(dbFolder.path, 'secure_app.db'));
// 获取存储在 Keychain/Keystore 中的加密密钥
const storage = FlutterSecureStorage();
var encryptionKey = await storage.read(key: 'db_encryption_key');
if (encryptionKey == null) {
// 首次启动:生成随机密钥
final random = List<int>.generate(32, (_) => Random.secure().nextInt(256));
encryptionKey = base64Encode(random);
await storage.write(key: 'db_encryption_key', value: encryptionKey);
}
return NativeDatabase(
file,
// SQLCipher 的 PRAGMA key 设置
setup: (rawDb) {
rawDb.execute("PRAGMA key = '$encryptionKey'");
},
);
});
}
七、各方案适用场景速查 需求 推荐方案 原因 应用配置(主题、语言) SharedPreferences简单键值,开箱即用 Token、密码等敏感数据 flutter_secure_storage硬件级加密存储 结构化数据 + 复杂查询 drift + SQLite完整 SQL 能力,关系模型 简单对象快速缓存 Hive轻量,纯 Dart 大量数据 + 复杂查询 Isar高性能,支持索引和全文搜索 需要加密的结构化数据 drift + SQLCipher加密 SQLite 跨设备同步 Isar + 自定义同步逻辑内置版本控制支持
八、过关自测 问题一 :Hive 和 Isar 的性能差异主要来自哪里?
Hive 是纯 Dart 实现,所有操作在 Dart VM 中执行,且查询需要全表扫描(无索引)。Isar 的核心是 C 编写的存储引擎(基于 LMDB),通过 FFI 调用,支持 B 树索引,查询路径极短。在百万级数据的场景下,有索引的 Isar 查询比 Hive 全表扫描快 10x 以上。
问题二 :设计一套离线缓存 + 在线同步的数据流架构。
推荐 Repository 模式:① 本地数据库作为单一数据源(SSOT),UI 只订阅本地 Stream;② 网络请求只更新本地数据库,不直接返回给 UI;③ 采用 Stale-While-Revalidate 策略——立即返回缓存,后台静默更新;④ 写操作采用乐观更新——先写本地,后台同步,失败时回滚;⑤ 在本地记录"脏数据"标记,网络恢复时批量同步。
问题三 :drift 的 Stream<List<Todo>> 如何实现数据变化通知?
drift 使用 SQLite 的"更新通知"机制(UpdateTracker)。每次执行 INSERT/UPDATE/DELETE 时,drift 记录哪些表被修改了。所有 watch 查询都订阅了特定表的变化通知,当该表被修改时,watch 查询会重新执行并将新结果推送到 Stream。这类似于 RxDB 或 Firebase Realtime Database 的实时更新,但基于本地 SQLite,无需网络。
总结 本地持久化的选择不是非此即彼的,同一个应用中往往会同时使用多种方案:
用户配置(主题/语言) → SharedPreferences
Token/密码 → flutter_secure_storage
业务数据(Todo/产品) → Isar 或 drift
大型媒体文件 → 文件系统(path_provider)
设计原则:最小权限、最简结构 。不要用关系型数据库存 Key-Value 配置,也不要用 SharedPreferences 存复杂对象。每种工具用在最适合它的地方,才能发挥最大价值。