news 2026/3/31 5:21:51

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/3/25 22:32:38

Figma转HTML完整教程:5分钟实现设计到代码的智能转换

Figma转HTML完整教程&#xff1a;5分钟实现设计到代码的智能转换 【免费下载链接】figma-html Builder.io for Figma: AI generation, export to code, import from web 项目地址: https://gitcode.com/gh_mirrors/fi/figma-html 在现代前端开发流程中&#xff0c;Figma…

作者头像 李华
网站建设 2026/3/11 4:14:45

B站视频下载神器:BilibiliDown全方位使用指南

B站视频下载神器&#xff1a;BilibiliDown全方位使用指南 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader &#x1f633; 项目地址: https://gitcode.com/gh_mirrors/bi/Bilibi…

作者头像 李华
网站建设 2026/3/29 4:59:07

Cursor AI编程助手试用限制的终极解决方案:从原理到实践

Cursor AI编程助手试用限制的终极解决方案&#xff1a;从原理到实践 【免费下载链接】go-cursor-help 解决Cursor在免费订阅期间出现以下提示的问题: Youve reached your trial request limit. / Too many free trial accounts used on this machine. Please upgrade to pro. W…

作者头像 李华
网站建设 2026/3/27 17:38:11

从视频到3D动作:开启低成本动作捕捉新时代

&#x1f3af; 痛点直击&#xff1a;传统动作捕捉的三大难题 【免费下载链接】VideoTo3dPoseAndBvh 项目地址: https://gitcode.com/gh_mirrors/vi/VideoTo3dPoseAndBvh "为什么我的动画角色动作如此僵硬&#xff1f;" 这可能是每个动画师都曾遇到的困扰。传…

作者头像 李华
网站建设 2026/3/25 7:49:08

11、Jenkins配置与分布式构建全解析

Jenkins配置与分布式构建全解析 1. Jenkins容器启动 在启动Jenkins容器之前,我们可以查看已有的Docker镜像,执行命令后输出如下: REPOSITORY TAG IMAGE ID CREATED SIZE jenkins/jenkins 2.73.1 c8a24e6775ea 24 hours ago …

作者头像 李华
网站建设 2026/3/28 9:22:37

16、Jenkins 实现持续集成与持续交付指南

Jenkins 实现持续集成与持续交付指南 1. 配置多分支管道 指定仓库所有者 :在“Owner”字段中,指定你的 GitHub 组织或用户账户名称。此时,“Repository”字段将列出你 GitHub 账户上的所有仓库。选择“hello-world-greeting”仓库。 设置构建配置 :滚动到“Build Conf…

作者头像 李华