用 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_flutter 的 NativeDatabase.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
相关推荐
云边云科技_云网融合4 分钟前
连锁门店组网怎么做?SD-WAN 多分支组网的架构选型、部署与成本结构
服务器·数据库·php
泡海椒21 分钟前
# 深度解析JQuick-SQL Join重排序与哈希连接执行机制
数据库·sql·哈希算法
acrel71 小时前
医院用电隐患治理与节能降耗及智慧能源平台应用
网络·数据库
snow@li1 小时前
PowerDesigner 全景教程:从业务建模、表结构设计、关系管理到正向/逆向、文档输出
数据库
ss2731 小时前
PostgreSQL表级锁、行级锁、轻量级锁
数据库·postgresql·区块链
RacentCLM2 小时前
证书即服务CaaS:从申请、续期到部署的自动化链路拆解
运维·数据库·自动化·ssl
SelectDB2 小时前
安全评审卡了三周,我把部署形态的四种选项重新排了一遍
大数据·数据库·云原生
Random Glow4 小时前
二级索引与回表:一次回表到底多贵
数据库·mysql·性能优化·innodb·索引
CarIise4 小时前
JDBC 与数据库事务核心
数据库·笔记