news 2026/9/3 3:03:16

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/9/2 22:32:37

OpenPLC项目应用:工业控制逻辑迁移实战

从“黑盒”到开源&#xff1a;我在产线改造中用树莓派跑通工业控制逻辑 你有没有经历过这样的场景&#xff1f;一台老设备突然停机&#xff0c;查了半天发现是PLC坏了。打电话给原厂&#xff0c;对方说&#xff1a;“这个型号已经停产了&#xff0c;备件要等三周。”——那一刻…

作者头像 李华
网站建设 2026/9/2 2:14:12

HTML转Figma完整指南:实现设计与代码的完美融合

HTML转Figma完整指南&#xff1a;实现设计与代码的完美融合 【免费下载链接】figma-html Builder.io for Figma: AI generation, export to code, import from web 项目地址: https://gitcode.com/gh_mirrors/fi/figma-html 还在为设计稿与前端代码之间的鸿沟而苦恼吗&a…

作者头像 李华
网站建设 2026/9/2 1:14:29

GPT-SoVITS语音合成在语音闹钟中的趣味应用

GPT-SoVITS语音合成在语音闹钟中的趣味应用 清晨六点半&#xff0c;卧室里响起的不再是刺耳的电子铃声&#xff0c;而是一段温柔熟悉的声音&#xff1a;“宝贝&#xff0c;太阳晒屁股啦&#xff01;”——语气像极了妈妈平时叫你起床的样子。这不是录音回放&#xff0c;而是由A…

作者头像 李华
网站建设 2026/9/3 0:25:55

3D视频转换新纪元:VR-Reversal免费工具深度解析

3D视频转换新纪元&#xff1a;VR-Reversal免费工具深度解析 【免费下载链接】VR-reversal VR-Reversal - Player for conversion of 3D video to 2D with optional saving of head tracking data and rendering out of 2D copies. 项目地址: https://gitcode.com/gh_mirrors/…

作者头像 李华
网站建设 2026/9/2 5:45:42

告别数据迷宫:3步构建专业级脑网络分析流水线

痛点诊断&#xff1a;脑网络研究者的真实困境 【免费下载链接】GRETNA A Graph-theoretical Network Analysis Toolkit in MATLAB 项目地址: https://gitcode.com/gh_mirrors/gr/GRETNA 作为一名脑网络研究者&#xff0c;你是否经常陷入这样的困境&#xff1a; 数据转换…

作者头像 李华
网站建设 2026/8/30 2:09:06

终极指南:ZLUDA如何让AMD GPU完美运行CUDA应用

终极指南&#xff1a;ZLUDA如何让AMD GPU完美运行CUDA应用 【免费下载链接】ZLUDA CUDA on AMD GPUs 项目地址: https://gitcode.com/gh_mirrors/zlu/ZLUDA 在GPU计算领域&#xff0c;CUDA长期以来都是NVIDIA的专利技术&#xff0c;这让AMD GPU用户无法享受到丰富的CUDA…

作者头像 李华