用 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
相关推荐
Tony Bai2 小时前
【API 设计之道】08 流量与配额:构建基于 Redis 的分布式限流器
数据库·redis·分布式·缓存
消失的旧时光-19432 小时前
Android(Kotlin) ↔ Flutter(Dart) 的“1:1 对应表”:架构分层来对照(MVVM/MVI 都适用)
android·flutter·kotlin
无名-CODING2 小时前
MyBatis 动态 SQL 全攻略
数据库·sql·mybatis
枫叶丹42 小时前
【Qt开发】Qt事件(二)-> QKeyEvent 按键事件
c语言·开发语言·数据库·c++·qt·microsoft
想学后端的前端工程师2 小时前
【Redis实战与高可用架构设计:从缓存到分布式锁的完整解决方案】
redis·分布式·缓存
llxxyy卢4 小时前
JWT安全&预编译CASE注入
数据库·sql·安全
大布布将军11 小时前
⚡️ 深入数据之海:SQL 基础与 ORM 的应用
前端·数据库·经验分享·sql·程序人生·面试·改行学it
子春一211 小时前
Flutter 2025 性能工程体系:从启动优化到帧率稳定,打造丝滑、省电、低内存的极致用户体验
flutter·ux
JIngJaneIL11 小时前
基于java+ vue农产投入线上管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot