A Kotlin Multiplatform library that adds persistent HTTP caching to Ktor HttpClient. Responses are stored on disk using Okio, with configurable size limits, TTL, and platform-appropriate cache directories.
- Persistent storage — Cache survives app restarts; stored on the filesystem via Okio.
- Kotlin Multiplatform — Shared API for Android, iOS, and JVM.
- Ktor integration — Uses Ktor’s HttpCache plugin; you configure cache storage and options in one place.
- Configurable — TTL (time-to-live), max cache size, directory name, shared vs unshared, and public vs private storage.
- Shared API surface — CacheStorageConfig models directory name, max size, and TTL;
CacheStorageFactory builds Ktor Cache Storage (
CacheStorage) with Okio persistence — the same backing store installPersistentCache uses whenenabledis true. - Content negotiation — Respects
Varyheaders so different variants (e.g. byAccept-Language) are cached separately. - LRU eviction — When the cache exceeds the configured size, least-recently-used entries are removed.
- Custom cache location — Optional CacheDirectoryProvider for custom cache root paths (e.g. for tests or special directories).
| Platform | Cache directory |
|---|---|
| Android | Application cache dir (context.cacheDir) |
| iOS | App caches directory (NSCachesDirectory in the sandbox) |
| JVM | java.io.tmpdir/ktor-cache |
- Kotlin 2.3.0+
- Ktor HttpClient (e.g.
ktor-client-core3.4.0+) and an engine (CIO, OkHttp, etc.) for your targets - Android: minSdk 24+, JDK 11+
- iOS: Standard deployment targets
- JVM: JDK 11+
Add the dependency to your shared or platform source sets.
Kotlin DSL (Gradle):
repositories {
mavenCentral()
// For snapshots:
// maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
dependencies {
commonMain.dependencies {
implementation("io.github.santimattius:ktor-persistent-cache:1.0.0-SNAPSHOT")
}
// Also add a Ktor engine for each target, e.g.:
// implementation("io.ktor:ktor-client-okhttp") // Android
// implementation("io.ktor:ktor-client-cio") // iOS / JVM
}Version catalog (e.g. libs.versions.toml):
[versions]
ktorPersistentCache = "1.0.0-SNAPSHOT"
[libraries]
ktor-persistent-cache = { group = "io.github.santimattius", name = "ktor-persistent-cache", version.ref = "ktorPersistentCache" }Then:
implementation(libs.ktor.persistent.cache)The library needs the application context to resolve the cache directory. The recommended way is App Startup:
-
Merge the library’s manifest
Theshared(or Android) module that depends onktor-persistent-cacheshould merge the library’s AndroidManifest so that the App StartupInitializationProviderandContextInitializerare registered. -
No extra code
If the manifest is merged,ContextInitializerruns at app startup and injects the application context. getCacheDirectoryProvider() will then use it automatically.
If you don’t use the library’s manifest (e.g. you use a different DI or startup path), you must call once at app startup with the application context (not an Activity context):
import io.github.santimattius.persistent.cache.startup.injectContext
// e.g. in Application.onCreate()
injectContext(applicationContext)injectContext is a public API in io.github.santimattius.persistent.cache.startup. It throws
IllegalArgumentException if you pass a context that can leak memory (for example an Activity).
No setup. The library uses the default app caches directory.
No setup. The library uses a subdirectory of the JVM temp directory.
- Create an HttpClient and call installPersistentCache with a CacheConfig:
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.github.santimattius.persistent.cache.*
val client = HttpClient(CIO) {
installPersistentCache(
CacheConfig(
enabled = true,
cacheDirectory = "http_cache",
maxCacheSize = 10L * 1024 * 1024, // 10 MB
cacheTtl = 60 * 60 * 1000, // 1 hour
isShared = true,
isPublic = false
)
)
}-
Use the client as usual. The cache stores responses for requests that support caching and serves them when valid.
Cross-restart persistence depends on the origin server's cacheability headers per RFC 7234 (for example
Cache-Control: max-age=…orExpires). This library persists whatever Ktor's HttpCache plugin stores; it does not override freshness rules. Responses markedno-storeare not written to disk.
val response: String = client.get("https://example.com/api/data").body()- Optionally pass a custom CacheDirectoryProvider as the second parameter ( see Custom cache directory).
CacheConfig supports:
| Property | Type | Default | Description |
|---|---|---|---|
enabled |
Boolean |
false |
Whether the HTTP cache is enabled. |
cacheDirectory |
String |
"http_cache" |
Name of the cache directory under the platform cache root. |
maxCacheSize |
Long |
10 MB | Maximum cache size in bytes. LRU eviction when exceeded. Use 0 for no limit. |
cacheTtl |
Long |
1 hour | Time-to-live for entries in milliseconds. Values <= 0 mean entries never expire (same convention as maxCacheSize <= 0 = unlimited). |
isShared |
Boolean |
true |
Whether the cache is shared across requests (Ktor HttpCache behavior). |
isPublic |
Boolean |
false |
When true, cached responses are treated as public (shareable across users); when false, they are private to the client. |
Convenience constructor:
CacheConfig(enabled = true, cacheDirectory = "my_cache")
// Other properties use defaults.This library installs Ktor's HttpCache and does not intercept the Auth pipeline. When you also install Auth, behavior follows Ktor's cache routing:
- Authenticated responses need
Cache-Control: private(for exampleprivate, max-age=3600) to be stored in private storage — the Okio-backed store configured byinstallPersistentCachewhenisPublic = false. Responses with onlymax-age(noprivate) route to Ktor's default public storage, which this helper does not configure. - Set
isShared = falseon CacheConfig when using Bearer (or other) auth on the same client. Ktor skips cache lookup for authorized requests on a shared client and refuses to store private entries whenisShared = true.
These rules are verified by AuthPluginInteropTest in the test suite. No extra configuration is
required beyond matching server cache headers and the CacheConfig flags above.
CacheStorageConfig covers only persisted disk knobs: subdirectory name under the platform (or custom) cache root, maximum size in bytes, and TTL in milliseconds.
CacheConfig
implements that shape and adds enabled, isShared, and isPublic, which installPersistentCache
respects alongside HttpCache.
CacheStorageFactory
produces Okio-backed CacheStorage. With enabled == true, installPersistentCache delegates here;
you can call the factory directly when configuring HttpCache yourself—for example with a fake
filesystem or deterministic clock in tests:
val storage = CacheStorageFactory.create(
config = CacheConfig(
enabled = true, // Ignored by the factory; use installPersistentCache to honor `enabled`
cacheDirectory = "http_cache",
maxCacheSize = 10L * 1024 * 1024,
cacheTtl = 60 * 60 * 1000,
),
cacheDirectoryProvider = getCacheDirectoryProvider()
)
HttpClient(CIO) {
install(HttpCache) {
isShared = true
privateStorage(storage)
}
}Important: Only installPersistentCache maps CacheConfig.enabled == false to CacheStorage.Disabled.
If you bypass it and always call CacheStorageFactory.create, you construct disk storage explicitly;
pair with your own HttpCache setup (or omit the plugin if you don’t want HTTP caching behavior).
To control where the cache is stored (e.g. a custom folder or test directory), implement CacheDirectoryProvider and pass it to installPersistentCache:
val customProvider = object : CacheDirectoryProvider {
override val cacheDirectory: Path get() = FileSystem.SYSTEM.toPath("/custom/cache/dir")
}
HttpClient(CIO) {
installPersistentCache(
config = CacheConfig(enabled = true, cacheDirectory = "http_cache"),
cacheDirectoryProvider = customProvider
)
}Default behavior (no custom provider): getCacheDirectoryProvider() returns the platform implementation (Android app cache dir, iOS caches dir, or JVM temp dir).
Build:
./gradlew :shared:compileKotlinIosSimulatorArm64 :shared:compileAndroidMain
# or
./gradlew :shared:assembleRun tests:
./gradlew testPublish to local Maven:
./gradlew :shared:publishToMavenLocalThen depend on io.github.santimattius:ktor-persistent-cache:1.0.0-SNAPSHOT with mavenLocal() in
your project.
The shared module is published with
the gradle-maven-publish-plugin.
| Action | Command / Doc |
|---|---|
| Publish to local Maven | ./gradlew :shared:publishToMavenLocal |
| Publish to Maven Central | See docs/PUBLISHING.md for credentials and steps. |
Coordinates and POM are configured in shared/build.gradle.kts.
This project is licensed under the Apache License, Version 2.0.
| Resource | URL |
|---|---|
| Ktor — HTTP client | ktor.io/docs/client |
| Ktor — Caching | ktor.io/docs/client-caching |
| Okio | github.com/square/okio |
| Kotlin Multiplatform | kotlinlang.org/docs/multiplatform |
| Publishing (this repo) | docs/PUBLISHING.md |