本文梳理 Kotlin Multiplatform 中网络请求缓存的读写方案:在 Ktor + Ktorfit 网络层之上,叠加”先读缓存、未命中再走网络、响应透明写回”的能力。代码与工程结构以 kmpDev 项目(
com.ctrip.flight.mmkv+ Ktor/Ktorfit)为准。配套文档:KMP 中 Ktor + Ktorfit 端到端集成(网络层初始化与本文化缓存的
Ktorfit/HttpClient/ 401 重试共用)。
架构总览
缓存位于 Data Layer 的 Repository 与 Ktorfit 之间,对上层(UseCase / ViewModel)透明。底层依赖两个能力:网络层(:core/network,见 dev-ktor.md)与存储层(:core/cache,封装 MMKV_KMP)。
1 | ┌──────────────────────────────────────────────────────────────────┐ |
模块归属一览
| 模块 | 职责 | 本文涉及 |
|---|---|---|
:core/cache |
MMKV 封装、CacheManager 接口与实现 |
initMMKV、CacheManager、MMKVCacheManagerImpl |
:core/network |
Ktor 客户端、CacheThenNetworkStrategy |
缓存读写策略、透明写插件 |
:shared / :feature:* |
Repository / UseCase | 调用策略 |
:androidApp / :iosApp |
平台壳层 | 应用入口 initMMKV |
iOS 代码在 kmpDev 设计阶段已就绪(Windows 下无法编译 iOS target,但源码存在,下述
iosMain与AppDelegate均为真实代码)。
一、添加依赖
kmpDev 使用 com.ctrip.flight.mmkv:mmkv-kotlin,仅需在 commonMain 添加一个依赖,Android / iOS 实现自动包含:
1 | # gradle/libs.versions.toml |
1 | // :core/build.gradle.kts |
iOS 额外要求:在
core/build.gradle.kts的cocoapods块中集成 MMKV 原生库:
1
2
3 cocoapods {
pod("MMKV") { version = "2.4.0" }
}
二、初始化
2.1 平台初始化入口(expect/actual)
各平台初始化 MMKV 的参数不同:Android 传 Context,iOS 传目录路径字符串。
1 | // :core/cache/CacheInit.kt(commonMain) |
1 | // :core/cache/CacheInit.android.kt(androidMain) |
1 | // :core/cache/CacheInit.ios.kt(iosMain) |
2.2 缓存抽象层(CacheManager 接口 + 元数据)
CacheManager 区分”通用 KV”与”网络缓存”。网络缓存以零转换原始 JSON 存进 MMKV,配合 CacheMeta(写入时间、TTL、版本号)做过期与升级清理。
1 | // :core/cache/CacheManager.kt(commonMain) |
1 | // :core/cache/CacheMeta.kt(commonMain) |
2.3 实现(MMKVCacheManagerImpl)
body 与 meta 分离存储:cache:body:<key> 存原始 JSON,cache:meta:<key> 存元数据。读时校验版本号与 TTL。
1 | // :core/cache/MMKVCacheManagerImpl.kt(commonMain) |
淘汰策略:不逐条 LRU。写入零开销;启动时
trimCache()检查mmkv.totalSize,超限扫描cache:meta:*按cachedAt排序删一半最旧的。trimCache()建议 App 启动延迟 2s 后调用(不阻塞冷启动)。版本升级 →clearAll()清空旧版本缓存。
三、双端初始化(平台壳层)
initMMKV 必须在任何 MMKV 读写之前调用。
Android(androidApp/.../App.kt):
1 | class App : Application() { |
iOS(iosApp/AppDelegate.swift):
1 | import shared |
调用顺序:①
initMMKV→ ②initLogStorage→ ③startKoin(Koin 注入CacheManager后可被业务使用)。
四、使用逻辑(从声明到调用)
4.1 缓存参数传递(CacheWriteConfig)
缓存写通过协程上下文在策略层 → 插件间传递,避免逐层透传参数。
1 | // :core/network/plugin/CacheWriteConfig.kt(commonMain) |
4.2 透明缓存写插件(HttpCachePlugin)
网络响应成功时,由插件把原始 JSON 透明写入 CacheManager。业务代码无感知。
1 | // :core/network/plugin/HttpCachePlugin.kt(commonMain) |
4.3 缓存读写策略(CacheThenNetworkStrategy)
这是网络请求缓存的核心。读缓存由策略直接调 cacheManager.getRawJson();写缓存通过 CacheWriteConfig.wrap { fetcher() } 注入上下文,由 4.2 的 HttpCachePlugin 在 onResponse 透明完成。
1 | // :core/network/strategy/CacheThenNetworkStrategy.kt(commonMain) |
配套的数据类:
CachePolicy(NO_CACHE/CACHE_THEN_NETWORK)、RequestResult(FromCache/FromNetwork/NetworkErrorWithCache/Error)均位于:core/cache与:core/network/model,详见 kmpDev 阶段文档。
4.4 Repository / UseCase 调用
1 | // :shared/data/UserRepository.kt(commonMain) |
4.5 ViewModel 收集
1 | // :feature:user/UserViewModel.kt(commonMain) |
至此形成完整闭环:
NetworkGraph.api<T>()提供 Ktorfit 接口 →CacheThenNetworkStrategy包裹(读getRawJson/ 写由HttpCachePlugin透明完成)→UserRepository对外暴露带缓存 Flow →UserViewModel收集。缓存逻辑全部在commonMain,与 UI 形态无关。
五、生成与部署命令
缓存不引入额外 KSP,构建命令与 dev-ktor.md 一致:
1 | # Android |
| 环境 | 说明 |
|---|---|
| macOS | 同时构建 Android + iOS |
| Windows | 仅 Android;iOS target 标红不影响 Android |
六、常见问题
Q1:缓存与 401 刷新冲突吗?
不冲突。401 由 AuthRetryHandler(见 dev-ktor.md)拦截刷新并重试原始请求;缓存写由 HttpCachePlugin 在成功响应后透明完成,重试后的新响应也会写入。
Q2:为什么缓存写要放在 Ktor 插件里、而不是在策略中手动 put?
写缓存通过 CacheWriteConfig 协程上下文传递,由 HttpCachePlugin.onResponse 自动写回,避免在每个 fetcher 里手写写缓存逻辑,也与 401 重试后的新响应自动对齐。
Q3:敏感数据能缓存吗?NO_CACHE 策略用于登录/支付等安全敏感接口;token 等走 SecureStorage(见 kmpDev 阶段三),不进网络缓存。
参考链接
- MMKV-Kotlin(ctrip):https://github.com/ctripcorp/mmkv-kotlin
- Kotlin 多平台存储实践:https://kotlinlang.org/docs/multiplatform-connect-to-apis.html
- 源码:https://github.com/daynearby/kmpdev