用 Drift 实现 Repository 无缝接入本地缓存/数据库(SWR:先快后准)

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_flutterNativeDatabase.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
相关推荐
倔强的石头_7 小时前
KingbaseES 新版MySQL 兼容版体验:旧版迁移 + 功能实测
数据库
恋猫de小郭20 小时前
Android 限制侧载新进展,谷歌联合国内厂商推验证计划
android·前端·flutter
恋猫de小郭20 小时前
解读 Android 17 全新内存限制,有没有“豁免”后门?
android·前端·flutter
倔强的石头_3 天前
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
数据库
程序员老刘3 天前
跨平台开发地图 | 2026年6月
flutter·ai编程·客户端
冬奇Lab4 天前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
悟空瞎说4 天前
Flutter 架构详解:新手必懂底层原理
flutter
SoaringHeart4 天前
Flutter最佳实践:IM聊天文字链接自动识别跳转
前端·flutter
ClouGence4 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
无响应de神4 天前
三、用户与权限管理
数据库·mysql