1)依赖与初始化(pubspec 思路)
常见组合(按你项目选):
driftdrift_flutter(Flutter 项目推荐)sqlite3_flutter_libs(iOS/Android 自带 sqlite)path_provider+path
(版本你用最新即可)
2)Drift 表结构:profiles
关键字段:
updatedAtMs用来做 TTL / 过期判断
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最省心。
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 提供这几个方法。
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
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 对应),下面示例:
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 负责策略。
abstract class ProfileApi { Future<ProfileModel> fetchProfile(String id); }7)Repository:DB 单一事实源 + refresh 回写(推荐)
7.1 watch:页面自动更新
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 有旧数据先出,再判断过期自动刷新:
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:
flutter pub run build_runner build --delete-conflicting-outputs