用 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
相关推荐
Data_Journal17 分钟前
如何将网页抓取用于机器学习
大数据·开发语言·数据库·python·scrapy
Lyra_Infra35 分钟前
从 MySQL 到达梦:一次信创隔离环境里的数据库迁移踩坑实录
数据库·后端·mysql
数据工匠老o35 分钟前
sysbench/TPC-C/自定义脚本:数据库压测工具对比与实战流程
数据库·测试
数安3000天1 小时前
数据分类分级产品有哪四类技术路线?
大数据·数据库
泡茶喝茶写代码1 小时前
量化数据开发实战系列(第 25 篇):基金基础接口实战:基金列表、ETF-LOF 分类、基金概况、净值数据
java·数据库·人工智能·python·mysql
张小姐的猫1 小时前
【AI大模型接入SDK】 —— 数据管理 & 与Session模块进行联动
数据结构·数据库·c++·人工智能·python·chatgpt
IvorySQL2 小时前
PostgreSQL 日报|大模型破解在线校验和(9 月 15 日)
数据库·人工智能·postgresql
●VON2 小时前
Flutter 鸿蒙插件适配实战:用 flutter_native_timezone_2025 1.0.1 读取当前时区与系统目录
flutter·华为·harmonyos·鸿蒙
Leon-Ning Liu2 小时前
Oracle 19c ASMB 进程 PGA 内存泄漏排查实战 (Bug 38610635)
数据库·oracle
步行cgn2 小时前
Spring 注入 null 和空字符串详解
数据库·spring·oracle