用 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
相关推荐
行者965 分钟前
Flutter与OpenHarmony跨平台分享组件深度实践
flutter·harmonyos·鸿蒙
行者9618 分钟前
Flutter跨平台开发在OpenHarmony上的评分组件实现与优化
开发语言·flutter·harmonyos·鸿蒙
Knight_AL44 分钟前
Spring 事务传播行为 + 事务失效原因 + 传播行为为什么不用其他模式
数据库·sql·spring
倔强的石头_1 小时前
时序数据时代的“存储与分析困局”解析及金仓解决方案
数据库
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue小型房屋租赁系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
倔强的石头_2 小时前
场景化落地指南——金仓时序数据库在关键行业的应用实践
数据库
SelectDB2 小时前
驾驭 CPU 与编译器:Apache Doris 实现极致性能的底层逻辑
运维·数据库·apache
zbguolei2 小时前
MySQL根据身份证号码计算出生日期和年龄
数据库·mysql
cn_mengbei2 小时前
Flutter for OpenHarmony 实战:IconButton 图标按钮详解
flutter
cn_mengbei3 小时前
Flutter for OpenHarmony 实战:OutlinedButton 边框按钮详解
flutter