1)依赖与初始化(pubspec 思路)
常见组合(按你项目选):
-
drift -
drift_flutter(Flutter 项目推荐) -
sqlite3_flutter_libs(iOS/Android 自带 sqlite) -
path_provider+path
(版本你用最新即可)
2)Drift 表结构:profiles
关键字段:
updatedAtMs用来做 TTL / 过期判断
Dart
import 'package:drift/drift.dart';
class Profiles extends Table {
TextColumn get id => text()(); // 主键
TextColumn get name => text()();
TextColumn get avatar => text().nullable()();
IntColumn get updatedAtMs => integer()(); // 记录更新时间(毫秒)
@override
Set<Column> get primaryKey => {id};
}
3)Database 定义(AppDatabase)
使用 drift_flutter 的 NativeDatabase.createInBackground 最省心。
Dart
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/drift.dart' as drift;
import 'package:drift_flutter/drift_flutter.dart';
part 'app_database.g.dart';
@DriftDatabase(tables: [Profiles], daos: [ProfileDao])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
return drift_flutter.openDatabase(
name: 'app.db',
native: const DriftNativeOptions(
shareAcrossIsolates: true,
),
);
});
}
说明:
-
part 'app_database.g.dart';需要 build_runner 生成 -
文件名你可以按你工程改,比如
db.dart
4)DAO:ProfileDao(watch + get + upsert)
Repository 最喜欢 DAO 提供这几个方法。
Dart
import 'package:drift/drift.dart';
import 'app_database.dart';
part 'profile_dao.g.dart';
@DriftAccessor(tables: [Profiles])
class ProfileDao extends DatabaseAccessor<AppDatabase> with _$ProfileDaoMixin {
ProfileDao(AppDatabase db) : super(db);
Stream<Profile?> watchProfile(String id) {
return (select(profiles)..where((t) => t.id.equals(id)))
.watchSingleOrNull();
}
Future<Profile?> getProfile(String id) {
return (select(profiles)..where((t) => t.id.equals(id)))
.getSingleOrNull();
}
Future<void> upsertProfile(ProfilesCompanion data) async {
await into(profiles).insertOnConflictUpdate(data);
}
Future<void> deleteProfile(String id) async {
await (delete(profiles)..where((t) => t.id.equals(id))).go();
}
Future<void> clearAll() async {
await delete(profiles).go();
}
}
5)Domain Model + Mapper(别省略,后期维护靠它)
Domain Model
Dart
class ProfileModel {
final String id;
final String name;
final String? avatar;
ProfileModel({required this.id, required this.name, this.avatar});
}
Mapper:Drift Row ↔ Domain
Drift 的 row 类型叫 Profile(与表名 Profiles 对应),下面示例:
Dart
import 'app_database.dart';
class ProfileMapper {
static ProfileModel toModel(Profile row) {
return ProfileModel(
id: row.id,
name: row.name,
avatar: row.avatar,
);
}
static ProfilesCompanion toCompanion(ProfileModel m) {
return ProfilesCompanion.insert(
id: m.id,
name: m.name,
avatar: Value(m.avatar),
updatedAtMs: DateTime.now().millisecondsSinceEpoch,
);
}
}
6)Remote API(Dio 获取网络数据)
接口层只负责"拿远端",Repository 负责策略。
Dart
abstract class ProfileApi {
Future<ProfileModel> fetchProfile(String id);
}
7)Repository:DB 单一事实源 + refresh 回写(推荐)
7.1 watch:页面自动更新
Dart
class ProfileRepository {
final ProfileApi api;
final ProfileDao dao;
ProfileRepository({required this.api, required this.dao});
Stream<ProfileModel?> watchProfile(String id) {
return dao.watchProfile(id).map((row) => row == null ? null : ProfileMapper.toModel(row));
}
Future<void> refreshProfile(String id) async {
final remote = await api.fetchProfile(id);
await dao.upsertProfile(ProfileMapper.toCompanion(remote));
}
}
页面使用方式(思路):
-
UI 订阅
watchProfile(id)→ 立即显示 DB 数据 -
下拉刷新调用
refreshProfile(id)→ 网络成功后写 DB → UI 自动更新
8)再加一层"TTL 过期策略"(先快后准 + 后台刷新)
如果你还想:DB 有旧数据先出,再判断过期自动刷新:
Dart
class CachePolicy {
final Duration ttl;
CachePolicy(this.ttl);
bool isExpired(int updatedAtMs) {
final age = DateTime.now().millisecondsSinceEpoch - updatedAtMs;
return age > ttl.inMilliseconds;
}
}
class ProfileRepositoryWithTtl {
final ProfileApi api;
final ProfileDao dao;
final CachePolicy policy;
ProfileRepositoryWithTtl({required this.api, required this.dao, required this.policy});
Stream<ProfileModel?> watchProfile(String id) {
return dao.watchProfile(id).map((row) => row == null ? null : ProfileMapper.toModel(row));
}
/// 页面进入时调用一次:如果过期就后台刷新
Future<void> refreshIfExpired(String id) async {
final cached = await dao.getProfile(id);
if (cached == null || policy.isExpired(cached.updatedAtMs)) {
await refreshProfile(id);
}
}
Future<void> refreshProfile(String id) async {
final remote = await api.fetchProfile(id);
await dao.upsertProfile(ProfileMapper.toCompanion(remote));
}
}
9)和 401 自动刷新 Token 如何衔接?
完全无感:
Repository 调 api.fetchProfile,Dio 层的 RefreshInterceptor 处理 401。
refresh 失败就触发全局 onAuthExpired,UI 统一跳登录,Repository 不管。
10)你需要生成代码(Drift 必做)
你有 part '*.g.dart' 的文件,需要 build:
Dart
flutter pub run build_runner build --delete-conflicting-outputs