Files
conti-docs/06-local-storage.md
T

159 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06. 本地存储方案
## 决策
按数据类型分三档存储,全部封装在 `core_storage` 包内,`feature_*` 不直接依赖底层存储库:
| 数据类型 | 方案 | 版本(2026-08 快照) |
|---|---|---|
| 结构化/关系型数据(门店列表缓存、订单历史等) | **[Drift](https://pub.dev/packages/drift)** | `^2.34.3` |
| 敏感数据(token、refresh token | **[flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage)** | `^11.0.0` |
| 简单非敏感 KV(是否看过引导页、用户偏好设置) | **[shared_preferences](https://pub.dev/packages/shared_preferences)** | `^2.5.5` |
## 依赖
```yaml
dependencies:
drift: ^2.34.3
sqlite3_flutter_libs: ^0.5.0
flutter_secure_storage: ^11.0.0
shared_preferences: ^2.5.5
dev_dependencies:
drift_dev: ^2.34.3
build_runner: ^2.4.0
```
## 使用规则
- 全仓库**只有一个 Drift 数据库实例**,定义在 `core_storage` 里,不允许每个 `feature_*` 各自建一个 SQLite 文件——避免多个数据库文件之间做跨 feature 查询/事务的麻烦。
- 每个 feature 拥有自己的表(`Table` 类)和 DAO`DriftAccessor`),表名加 feature 前缀(如 `store_cache``payment_history`)避免命名冲突,但都注册进同一个 `AppDatabase`
- feature 的 `data``local_datasource` 只依赖自己的 DAO 类型,不直接操作 `AppDatabase` 或访问其他 feature 的表。
- token / refresh token 只能经过 `core_auth` 包里封装的 secure storage 读写方法,不允许其他 `core_*`/`feature_*` 直接调用 `FlutterSecureStorage` 实例。
- 数据库表结构变更必须写 migration(`onUpgrade` + `schemaVersion` 递增),不允许直接改字段定义后期望"重装了事"——线上用户已有数据需要平滑迁移。
## 参考链接
- [Drift 官方文档](https://drift.simonbinder.eu/)
- [drift | Dart package](https://pub.dev/packages/drift)
- [flutter_secure_storage | Dart package](https://pub.dev/packages/flutter_secure_storage)
- [shared_preferences | Dart package](https://pub.dev/packages/shared_preferences)
## 附录:Drift 是什么,日常怎么用
给还没接触过这套本地数据库封装方式的同学看的入门说明。
### 要解决的问题
Flutter 生态里直接操作本地 SQLite 最常见的是 [`sqflite`](https://pub.dev/packages/sqflite),但它是纯 SQL 字符串拼接:
```dart
// sqflite 写法,容易手滑打错字段名/表名,编译期完全发现不了
await db.rawQuery('SELECT * FROM stroe WHERE nmae = ?', [name]);
```
字段名、表名全靠字符串,拼错了只有运行时才报错;查询结果是 `Map<String, Object?>`,还得手动转成业务对象;数据变化了想让 UI 自动刷新,也得自己手写一套通知机制。
**Drift**`sqflite`(或更底层的 `sqlite3`)之上加了一层代码生成:用 Dart 类定义表结构,`build_runner` 生成类型安全的查询代码,写错字段名/类型在编译期就会报错;查询结果直接是强类型的 Dart 对象;还内置了 `.watch()` 方法,数据变化时自动推送新结果,天然适合配合 Riverpod 的 `StreamProvider`/`AsyncNotifier` 做响应式 UI。
### 核心概念
1. **`Table` 类**:用 Dart 代码声明表结构(字段名、类型、约束),而不是手写 `CREATE TABLE` 语句。
2. **`DriftAccessor`(DAO)**:给一组相关表写查询/增删改方法的地方,业务代码只调用 DAO 方法,不直接写 SQL。
3. **`.watch()` vs `.get()`**`.get()` 是一次性查询,`.watch()` 返回一个 `Stream`,只要底层数据变化(哪怕是另一个页面改的)就会自动推送新结果——不需要手动刷新。
4. **`schemaVersion` + `onUpgrade`**:数据库版本号和迁移回调,改表结构时递增版本号并在 `onUpgrade` 里写迁移逻辑(加字段、建索引等),保证已安装用户的本地数据不会因为升级直接报错或丢失。
### 使用示例(`feature_store`:门店列表本地缓存)
```dart
// packages/core_storage/lib/src/tables/store_table.dart
class StoreCache extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
RealColumn get lat => real()();
RealColumn get lng => real()();
DateTimeColumn get cachedAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
```
```dart
// packages/core_storage/lib/src/daos/store_dao.dart
part 'store_dao.g.dart';
@DriftAccessor(tables: [StoreCache])
class StoreDao extends DatabaseAccessor<AppDatabase> with _$StoreDaoMixin {
StoreDao(super.db);
Future<void> upsertAll(List<StoreCacheCompanion> stores) =>
batch((b) => b.insertAllOnConflictUpdate(storeCache, stores));
Stream<List<StoreCacheData>> watchAll() => select(storeCache).watch();
}
```
```dart
// packages/core_storage/lib/src/app_database.dart
@DriftDatabase(tables: [StoreCache, PaymentHistory], daos: [StoreDao, PaymentHistoryDao])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (m, from, to) async {
if (from < 2) {
await m.addColumn(storeCache, storeCache.cachedAt);
}
},
);
}
```
```dart
// feature_store 的 local_datasource 只依赖 StoreDao,不直接碰 AppDatabase
class StoreLocalDataSource {
final StoreDao _dao;
StoreLocalDataSource(this._dao);
Stream<List<Store>> watchCachedStores() =>
_dao.watchAll().map((rows) => rows.map(Store.fromCacheRow).toList());
}
```
配合 Riverpod 做响应式 UI(离线也能展示上次缓存的门店列表,等网络数据回来再刷新):
```dart
@riverpod
Stream<List<Store>> cachedStores(Ref ref) {
final localDataSource = ref.watch(storeLocalDataSourceProvider);
return localDataSource.watchCachedStores();
}
```
### secure storage 使用示例(token 存取)
```dart
// packages/core_auth/lib/src/token_storage.dart
class TokenStorage {
final FlutterSecureStorage _storage;
TokenStorage(this._storage);
Future<void> saveTokens({required String accessToken, required String refreshToken}) =>
Future.wait([
_storage.write(key: 'access_token', value: accessToken),
_storage.write(key: 'refresh_token', value: refreshToken),
]);
Future<String?> readAccessToken() => _storage.read(key: 'access_token');
Future<void> clear() => _storage.deleteAll();
}
```
`TokenStorage` 是全仓库唯一直接持有 `FlutterSecureStorage` 实例的类,其他包只能通过 `core_auth` 暴露的 provider 间接读写 token。