news 2026/4/18 12:17:18

Repository 层如何无缝接入本地缓存 / 数据库

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Repository 层如何无缝接入本地缓存 / 数据库

——一套“先快后准”的数据策略:Memory → DB → Network → 回写

目标:页面打开秒出数据(缓存/数据库),后台再拉取网络数据更新;弱网/离线也能用;Repository 对上层只暴露干净的领域模型,不让 UI 知道缓存细节。

1)为什么缓存/数据库要放在 Repository?

Repository 的职责是:对业务提供“数据真相”
它应该屏蔽数据来自哪里:

  • 网络:Dio / REST

  • 本地:SQLite/Isar/Hive/SharedPreferences

  • 内存:Map/LRU

  • 组合策略:先缓存后网络、过期刷新、离线兜底

UI/State 层只关心:

“我需要 profile 数据”,“我刷新一下”,“我加载更多”。

2)推荐目录结构(可复用)

data/ remote/ api_client.dart profile_api.dart local/ db.dart profile_dao.dart entities/ profile_entity.dart repository/ profile_repository.dart domain/ models/ profile.dart mappers/ profile_mapper.dart
  • Entity:数据库结构(字段可能更偏存储)
  • Model:业务模型(给 UI/UseCase 用)
  • Mapper:Entity ↔ Model 的转换
  • Repository:组合 Network + DB + Cache 策略

3)策略选型:Cache-Aside + Stale-While-Revalidate(最常用)

你想要的“无缝”体验,最常用就是这套:

  1. 先读缓存/DB(快)→ 立即返回给 UI
  2. 同时/随后拉Network(准)
  3. 成功后回写 DB/Cache并通知 UI 更新

这就是经典的:

  • Cache-aside(缓存旁路)
  • SWR(过期可用 + 后台刷新)

4)核心接口:Repository 对外暴露什么?

强烈建议 Repository 对外提供两种能力:

A. 一次性读取(适合简单页面)

Future<Profile> getProfile({bool forceRefresh = false});

B. 流式订阅(推荐:DB 作为单一事实来源)

Stream<Profile> watchProfile();
Future<void> refreshProfile();

如果你想“秒出 + 自动刷新后 UI 自动更新”,选B会更爽。

5)实现方案 1:DB 为单一事实源(推荐中大型项目)

思路:UI 只订阅 DB,Repository 负责刷新并回写 DB。

5.1 Domain Model(业务模型)

class Profile { final String id; final String name; final String avatar; Profile({required this.id, required this.name, required this.avatar}); }

5.2 DB Entity(存储结构)

class ProfileEntity { final String id; final String name; final String avatar; final int updatedAtMs; ProfileEntity({ required this.id, required this.name, required this.avatar, required this.updatedAtMs, }); }

5.3 Mapper(Entity ↔ Model)

class ProfileMapper { static Profile toModel(ProfileEntity e) => Profile(id: e.id, name: e.name, avatar: e.avatar); static ProfileEntity toEntity(Profile m) => ProfileEntity( id: m.id, name: m.name, avatar: m.avatar, updatedAtMs: DateTime.now().millisecondsSinceEpoch, ); }

5.4 DAO(你用 Drift/Isar/Hive 都行,这里只给接口)

abstract class ProfileDao { Stream<ProfileEntity?> watch(); Future<ProfileEntity?> get(); Future<void> upsert(ProfileEntity entity); Future<void> clear(); }

5.5 Remote API

abstract class ProfileApi { Future<Profile> fetchProfile(); }

5.6 Repository(重点:策略实现)

class ProfileRepository { final ProfileApi api; final ProfileDao dao; ProfileRepository({required this.api, required this.dao}); Stream<Profile?> watchProfile() { return dao.watch().map((e) => e == null ? null : ProfileMapper.toModel(e)); } Future<void> refreshProfile() async { final profile = await api.fetchProfile(); await dao.upsert(ProfileMapper.toEntity(profile)); } Future<Profile?> getCachedProfile() async { final e = await dao.get(); return e == null ? null : ProfileMapper.toModel(e); } }

UI 用法:

  • 页面订阅watchProfile()
  • 页面下拉刷新调用refreshProfile()
  • 弱网下仍有 DB 数据兜底

6)实现方案 2:一次性读取 + TTL(适合小中型项目)

如果你暂时不想用 Stream(或 DB 不支持 watch),用 TTL 也很常见:

6.1 定义缓存策略

  • 内存缓存:秒开
  • DB 缓存:离线兜底
  • TTL:比如 10 分钟过期
class CachePolicy { final Duration ttl; CachePolicy(this.ttl); bool isExpired(int updatedAtMs) { final age = DateTime.now().millisecondsSinceEpoch - updatedAtMs; return age > ttl.inMilliseconds; } }

6.2 Repository:先快后准

class ProfileRepository2 { final ProfileApi api; final ProfileDao dao; final CachePolicy policy; ProfileRepository2({required this.api, required this.dao, required this.policy}); Future<Profile> getProfile({bool forceRefresh = false}) async { final cached = await dao.get(); if (!forceRefresh && cached != null && !policy.isExpired(cached.updatedAtMs)) { // 未过期:直接用本地 return ProfileMapper.toModel(cached); } try { // 过期/强刷:走网络 final remote = await api.fetchProfile(); await dao.upsert(ProfileMapper.toEntity(remote)); return remote; } catch (_) { // 网络失败:兜底用旧缓存(只要有) if (cached != null) return ProfileMapper.toModel(cached); rethrow; } } }

这就是“过期刷新 + 失败回退”。

7)如何“无缝接入 401 自动刷新 Token”?

Repository 不需要知道 token 刷新逻辑。
你只要保证 Dio 层有:

  • AuthInterceptor 注入 token

  • RefreshInterceptor / QueueRefreshInterceptor 处理 401

Repository 仍旧只是:

final profile = await api.fetchProfile();

登录过期(refresh 失败)由全局onAuthExpired统一处理即可。

8)工程建议:你最容易踩的 4 个坑

  1. UI 直接读 DB + 直接调 API:会绕过 Repository,逻辑散落

  2. 没有 Mapper:Entity/Model 混用,后期字段调整会痛苦

  3. 缓存失效策略缺失:要么永远旧,要么永远打网

  4. 写入时机不统一:建议所有网络成功的数据都回写 DB,DB 成事实源

9)你该怎么选(给你一句话)

  • 想要“秒开 + 自动更新 + 离线可用” ✅方案 1:DB 单一事实源 + watch

  • 项目小、只想快速落地 ✅方案 2:TTL + 失败回退

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 18:47:13

Nodejs订阅nats主题

我来帮您了解 Node.js 中订阅 NATS 主题的相关信息&#xff0c;包括基本概念、实现方法和示例代码。 我来为您详细讲解 Node.js 中订阅 NATS 主题的相关知识&#xff0c;包括基础用法和实际示例。安装 NATS 客户端 npm install nats基础订阅示例 const { connect, StringCodec …

作者头像 李华
网站建设 2026/4/18 8:38:42

零基础实战:Tiptap+Hocuspocus构建高效协作编辑系统

零基础实战&#xff1a;TiptapHocuspocus构建高效协作编辑系统 【免费下载链接】tiptap 项目地址: https://gitcode.com/gh_mirrors/tip/tiptap 还在为团队文档协作的混乱局面头疼&#xff1f;多人同时编辑时格式错乱、内容冲突、版本丢失&#xff1f;今天带你用最简方…

作者头像 李华
网站建设 2026/4/18 7:53:22

2、软件开发中的持续集成:从传统到敏捷的转变

软件开发中的持续集成:从传统到敏捷的转变 1. 软件开发生命周期概述 软件开发生命周期(SDLC)是规划、开发、测试和部署软件的过程。团队遵循一系列阶段,每个阶段都依赖前一阶段的成果。以下是SDLC的详细阶段: 1. 需求分析 :业务团队(主要由业务分析师组成)对项目的…

作者头像 李华
网站建设 2026/4/17 22:02:27

7、Jenkins 安装与新特性全解析

Jenkins 安装与新特性全解析 一、Jenkins 数据卷操作与实例创建 在使用 Docker 部署 Jenkins 时,我们可以通过数据卷来管理 Jenkins 的数据。以下是具体的操作步骤: 1. 查看数据卷 当我们执行相应操作后,会看到如下输出: | DRIVER | VOLUME NAME | | ---- | ---- | …

作者头像 李华
网站建设 2026/4/18 2:23:03

ZLUDA:在AMD显卡上运行CUDA应用的完整指南

ZLUDA&#xff1a;在AMD显卡上运行CUDA应用的完整指南 【免费下载链接】ZLUDA CUDA on AMD GPUs 项目地址: https://gitcode.com/gh_mirrors/zlu/ZLUDA ZLUDA是一个开源项目&#xff0c;专门为AMD GPU设计CUDA兼容层。该项目通过智能转译机制&#xff0c;让用户能够在AM…

作者头像 李华
网站建设 2026/4/18 7:44:05

18、使用 Jenkins 实现持续部署及相关配置指南

使用 Jenkins 实现持续部署及相关配置指南 1. 前期准备 Ubuntu/Debian 用户可能需要安装 dkms 包,以确保在下次 apt-get upgrade 期间 Linux 内核版本更改时,VirtualBox 主机内核模块(vboxdrv、vboxnetflt 和 vboxnetadp)能正确更新。可以通过 Synaptic 包管理器或以下…

作者头像 李华