KStorage is a lightweight Kotlin Multiplatform library that provides a unified and type-safe interface for key-value storage across different platforms. It simplifies the process of storing and retrieving data by abstracting the underlying storage mechanisms.
- Multiplatform Support: Works across Android, iOS, JVM, JS, Linux, macOS, Windows, tvOS, watchOS and WasmJS.
- Type-Safe API: Ensures compile-time type checking for stored values.
- Thread-Safe by Default: Every Krate is protected by a built-in platform-specific lock β no manual synchronization needed.
- Reactive: First-class
StateFlowandFlowsupport for observing value changes. - Lightweight: Minimal overhead with a focus on simplicity and performance.
- Extensible: Easily integrate with any storage backend β SharedPreferences, DataStore, ROOM, files, or your own.
[versions]
klibs-kstorage = "<latest-version>"
[libraries]
klibs-kstorage = { module = "ru.astrainteractive.klibs:kstorage", version.ref = "klibs-kstorage" }implementation("ru.astrainteractive.klibs:kstorage:<version>")
// or version catalogs
implementation(libs.klibs.kstorage)Use MutableKrate when your storage is fast and synchronous (e.g. in-memory maps,
SharedPreferences):
class SettingsApi(private val settings: MutableMap<String, Int>) {
// Basic mutable krate β reads/writes are synchronous
val volumeKrate: MutableKrate<Int> = DefaultMutableKrate(
factory = { 50 },
loader = { settings["volume"] },
saver = { value -> settings["volume"] = value }
)
}The factory provides the default value when the loader returns null. That's it β three lambdas
and you have
a fully thread-safe, type-safe storage accessor.
Wrap any Krate to avoid repeated loads:
val cachedVolume: CachedMutableKrate<Int> = volumeKrate.asCachedMutableKrate()
// Access the in-memory cached value directly
val current = cachedVolume.cachedValueNeed to observe changes in your UI? Wrap into a StateFlowMutableKrate:
val reactiveVolume: StateFlowMutableKrate<Int> = volumeKrate.asStateFlowMutableKrate()
// Collect in your ViewModel / Compose / SwiftUI
reactiveVolume.cachedStateFlow.collect { volume ->
println("Volume changed: $volume")
}Don't want to call .cachedValue every time? Use Kotlin's by delegation:
val volume by reactiveVolume // delegates to cachedStateFlow.value
val cached by cachedVolume // delegates to cachedValueUse SuspendMutableKrate for I/O-bound storage (databases, files, network):
class SuspendSettingsApi(private val settings: MutableMap<String, Int>) {
val volumeKrate: SuspendMutableKrate<Int> = DefaultSuspendMutableKrate(
factory = { 50 },
loader = { settings["volume"] },
saver = { value -> settings["volume"] = value }
)
}The API mirrors the blocking version, but every operation is suspend:
// Save a value
volumeKrate.save(75)
// Transform and save
volumeKrate.save { current -> current + 10 }
// Transform, save, and get the new value
val newVolume = volumeKrate.saveAndGet { current -> current + 10 }
// Reset to factory default
volumeKrate.reset()Combine suspend operations with StateFlow observation:
val reactiveVolume: StateFlowSuspendMutableKrate<Int> = DefaultStateFlowSuspendMutableKrate(
factory = { 50 },
loader = { settings["volume"] },
saver = { value -> settings["volume"] = value }
)
// Observe reactively
reactiveVolume.cachedStateFlow.collect { volume ->
println("Volume: $volume")
}
// Write asynchronously
reactiveVolume.save(100)Use FlowMutableKrate to integrate with Jetpack DataStore or any Flow-based backend:
class FlowSettingsApi {
private val key = intPreferencesKey("volume")
private val dataStore: DataStore<Preferences> = /* ... */
val volumeKrate: FlowMutableKrate<Int> = DefaultFlowMutableKrate(
factory = { 50 },
loader = { dataStore.data.map { it[key] } },
saver = { value -> dataStore.edit { it[key] = value } }
)
// Convert to StateFlow when needed
val stateFlow: StateFlow<Int> = volumeKrate.stateFlow(viewModelScope)
}You can even convert a FlowMutableKrate into a StateFlowSuspendMutableKrate:
val reactiveKrate = volumeKrate.asStateFlowSuspendMutableKrate(viewModelScope)Every Krate is thread-safe by default. You don't need to add any synchronization yourself.
On multithreaded platforms, each Krate is backed by a platform-specific recursive lock:
| Platform | Lock Implementation |
|---|---|
| JVM / Android | ReentrantLock |
| iOS / macOS / tvOS / watchOS | NSRecursiveLock |
| Linux / MinGW | Recursive pthread_mutex |
On JS / WasmJS there is no real locking β the runtime is single-threaded, so the Lock
implementation is a
lightweight no-op wrapper that simply delegates to the block directly.
- Every Krate internally implements
LockOwner, which holds aLockinstance. - All read and write operations are automatically wrapped in
withLock {}(blocking) orwithSuspendLock {}(suspend). - Nested wrappers share the same lock β when you call
.asCachedMutableKrate()or.asStateFlowMutableKrate(), the wrapper reuses the lock from the original Krate viaLockOwner.Reusable. This prevents deadlocks and keeps everything consistent.
val krate: MutableKrate<Int> = DefaultMutableKrate(
factory = { 0 },
loader = { settings["key"] },
saver = { value -> settings["key"] = value }
)
// The cached wrapper shares the same lock β no deadlocks, no double-locking
val cached = krate.asCachedMutableKrate()
val stateFlow = krate.asStateFlowMutableKrate()You can safely read and write from multiple threads or coroutines without any additional synchronization:
// Safe to call from any thread
coroutineScope {
repeat(100) {
launch(Dispatchers.Default) {
krate.save { current -> current + 1 }
}
}
}
// krate.getValue() == 100 β
Have a nullable Krate but need a non-null one somewhere? Use .withDefault:
val nullableKrate: MutableKrate<Int?> = DefaultMutableKrate(
factory = { null },
loader = { settings["key"] },
saver = { value ->
if (value == null) settings.remove("key")
else settings["key"] = value
}
)
// Wrap it β getValue() will never return null
val nonNullKrate: MutableKrate<Int> = nullableKrate.withDefault { 42 }.withDefault is available on every Krate type β Krate, MutableKrate, SuspendKrate,
SuspendMutableKrate, FlowKrate, FlowMutableKrate, StateFlowKrate,
StateFlowSuspendKrate, and StateFlowSuspendMutableKrate.
Need a quick in-memory store for testing or caching?
// Blocking
val counter: MutableKrate<Int> = InMemoryMutableKrate { 0 }
// Suspend
val asyncCounter: SuspendMutableKrate<Int> = InMemorySuspendMutableKrate { 0 }Encapsulate a Krate as a dedicated class using delegation:
class IntKrate(
key: String,
settings: MutableMap<String, Int>
) : MutableKrate<Int?> by DefaultMutableKrate(
factory = { null },
loader = { settings[key] },
saver = { value -> settings[key] = value }
)Store any type by mapping to/from the underlying storage format:
data class UserSettings(val theme: String, val fontSize: Int)
class UserSettingsKrate(
jsonStore: MutableMap<String, String>,
json: Json
) : MutableKrate<UserSettings> by DefaultMutableKrate(
factory = { UserSettings(theme = "light", fontSize = 14) },
loader = {
jsonStore["user_settings"]?.let { json.decodeFromString(it) }
},
saver = { value ->
jsonStore["user_settings"] = json.encodeToString(value)
}
)Create Krates on-the-fly for different entities:
fun scoreKrateForUser(userId: String): MutableKrate<Int> {
return DefaultMutableKrate(
factory = { 0 },
loader = { settings["score_$userId"] },
saver = { value -> settings["score_$userId"] = value }
)
}The real power of KStorage is the ability to layer behaviors:
val krate: MutableKrate<Int?> = DefaultMutableKrate(
factory = { null },
loader = { settings["key"] },
saver = { value -> settings["key"] = value }
)
// 1. Make it non-null
val nonNull: MutableKrate<Int> = krate.withDefault { 0 }
// 2. Add in-memory caching
val cached: CachedMutableKrate<Int> = nonNull.asCachedMutableKrate()
// 3. Make it reactive
val reactive: StateFlowMutableKrate<Int> = nonNull.asStateFlowMutableKrate()All wrappers share the same underlying lock, so the entire chain is thread-safe.
KStorage supports a wide range of Kotlin Multiplatform targets:
- JVM / Android
- iOS (x64, arm64, simulator)
- macOS (x64, arm64)
- tvOS (x64, arm64, simulator)
- watchOS (x64, arm64, simulator)
- Linux (x64)
- Windows (mingwX64)
- JS (IR)
- WasmJS