
Provides a simple, type-safe key-value storage solution offering thread-safe operations, atomic updates, and a clean API for storing common data types.
KvsStorage is a Kotlin Multiplatform library that provides a simple, type-safe key-value storage solution. It's designed to work across multiple platforms including Android, iOS, and other Kotlin Multiplatform targets.
The library offers:
Add the following to your shared module's build.gradle.kts:
val kvsVersion = "1.0.0" // Check for the latest version
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.santimattius:kvs:$kvsVersion")
}
}
}// In your Application class or dependency injection module
private val kvs = Storage.kvs("user_preferences")// In your AppDelegate or dependency injection setup
private let kvs = Storage.shared.kvs(name: "user_preferences")You can also initialize an encrypted KVS on each platform:
// In your Application class or dependency injection module
private val kvs = Storage.encryptKvs(name = "user_preferences", key = "secret")// In your AppDelegate or dependency injection setup
private let kvs = Storage.shared.encryptKvs(name: "user_pref", key: "secret")// Using the editor pattern for multiple operations
kvs.edit()
.putString("username", "john_doe")
.putInt("user_age", 30)
.putBoolean("is_premium", true)
.commit() // Commits all changes atomically// Launch in a coroutine scope
lifecycleScope.launch {
val username = kvs.getString("username", "default_user")
val age = kvs.getInt("user_age", 0)
val isPremium = kvs.getBoolean("is_premium", false)
// Get all stored values
val allValues = kvs.getAll()
}// Remove a single key
kvs.edit()
.remove("username")
.commit()
// Clear all data
kvs.edit()
.clear()
.commit()Store and retrieve a single serializable object (e.g., a profile) as a document.
import kotlinx.serialization.Serializable
@Serializable
data class Profile(val username: String, val email: String)
val document = Storage.document("profile")
// val document = Storage.encryptDocument("profile", "secret")
val userProfile = Profile(username = "santimattius", email = "email@example.com")
document.put(userProfile)
val result: Profile? = document.get()struct Profile: Codable { let username: String; let email: String }
let document = Storage.shared.document(name: "profile")
// let document = Storage.shared.encryptDocument(name: "profile", secretKey: "secret")
let profile = Profile(username: "santimattius", email: "email@example.com")
try await document.put(value: profile)
let saved: Profile = try await document.get()For testing purposes, you can use InMemoryKvs which provides a volatile, in-memory implementation of the KVS interface. This is particularly useful for unit tests where you don't want to persist any data between test runs.
// In your test class (commonTest)
private val testKvs = Storage.inMemoryKvs("test_preferences")
@Test
fun `test kvs operations`() = runTest {
// Store data
testKvs.edit()
.putString("test_key", "test_value")
.commit()
// Retrieve data
val value = testKvs.getString("test_key", "default")
assertEquals("test_value", value)
// Clear data between tests
testKvs.edit().clear().commit()
}
// On iOS (Swift)
// private let testKvs = Storage.shared.inMemoryKvs(name: "test_preferences")Experimental: The TTL API is experimental and may change in future releases. Use @OptIn(ExperimentalKvsTtl::class) when using TTL in Kotlin.
Storage with TTL allows keys to expire after a duration. You can set a default TTL for the instance and optionally override it per key.
@OptIn(ExperimentalKvsTtl::class)
fun createCache() {
val ttl = object : Ttl {
override fun value() = kotlin.time.Duration.ofHours(1).inWholeMilliseconds
}
val cache = Storage.kvs("cache", ttl = ttl)
// Uses default TTL (1 hour)
cache.edit().putString("key1", "value1").commit()
// Override TTL for this key (30 minutes)
cache.edit().putString("key2", "value2", kotlin.time.Duration.ofMinutes(30)).commit()
// Expired keys return the default value; cleanup runs in batch via getAll() or CleanupJob
val value = cache.getString("key1", "default")
// Optional: background cleanup job (recommended for TTL storage)
cache.cleanupJob(kotlin.time.Duration.ofMinutes(10)).start(applicationScope)
}| API | Description |
|---|---|
Storage.kvs(name, ttl, encrypted) |
Creates a [KvsExtended] instance with optional default TTL and encryption. |
KvsExtended.edit().putString(key, value, duration) |
Overloads with duration set per-key TTL. |
KvsExtended.cleanupJob(interval) |
Returns a job that periodically removes expired keys on a background dispatcher. |
lifecycleScope.launch {
val hasUsername = kvs.contains("username")
}kvs.edit().apply {
// Multiple operations
putString("key1", "value1")
putInt("key2", 42)
remove("old_key")
}.commit() // All operations are atomicReact to value changes in keys using platform-native streams:
// Observe a single key
lifecycleScope.launch {
kvs.getStringAsStream("username", "default_user").collect { username ->
// React to username changes
}
}
// Other types
lifecycleScope.launch {
kvs.getIntAsStream("user_age", 0).collect { age -> }
}
lifecycleScope.launch {
kvs.getFloatAsStream("user_score", 0f).collect { score -> }
}
lifecycleScope.launch {
kvs.getBooleanAsStream("is_premium", false).collect { isPremium -> }
}
lifecycleScope.launch {
kvs.getLongAsStream("last_login", 0L).collect { lastLogin -> }
}
// Observe all values as a map
lifecycleScope.launch {
kvs.getAllAsStream().collect { allValues ->
// allValues: Map<String, Any?>
}
}// Observe a single key
Task {
for await username in kvs.getStringAsStream(name: "username", defaultValue: "default_user") {
// React to username changes
}
}
// Other types
Task {
for await age in kvs.getIntAsStream(name: "user_age", defaultValue: 0) {}
}
Task {
for await score in kvs.getFloatAsStream(name: "user_score", defaultValue: 0.0) {}
}
Task {
for await isPremium in kvs.getBooleanAsStream(name: "is_premium", defaultValue: false) {}
}
Task {
for await lastLogin in kvs.getLongAsStream(name: "last_login", defaultValue: 0) {}
}
// Observe all values as a dictionary
Task {
for await allValues in kvs.getAllAsStream() {
// allValues: [String: Any?]
}
}The library provides helper functions that wrap reads and edits into platform-native Result types to simplify error handling.
Available functions in shared/src/commonMain/kotlin/com/santimattius/kvs/KvsExtensions.kt:
suspend fun Kvs.getStringAsResult(key: String, defValue: String): Result<String>suspend fun Kvs.getIntAsResult(key: String, defValue: Int): Result<Int>suspend fun Kvs.getLongAsResult(key: String, defValue: Long): Result<Long>suspend fun Kvs.getFloatAsResult(key: String, defValue: Float): Result<Float>suspend fun Kvs.getBooleanAsResult(key: String, defValue: Boolean): Result<Boolean>suspend fun Kvs.getAllAsResult(): Result<Map<String, Any>>suspend fun Kvs.KvsEditor.apply(): Result<Boolean>Example usage:
lifecycleScope.launch {
val result = kvs.getStringAsResult("username", "guest")
result
.onSuccess { username -> /* use username */ }
.onFailure { error -> /* handle error */ }
val save = kvs.edit()
.putString("username", "john")
.putBoolean("is_premium", true)
.apply() // Result<Boolean>
save.onSuccess { /* committed */ }
.onFailure { e -> /* handle commit error */ }
}Note: these helpers execute in a non-cancellable context internally to ensure completion semantics and return failures as Result without throwing.
Available functions in shared/src/commonMain/swift/KvsExtensions.swift:
func getStringAsResult(key: String, defValue: String) async -> Result<String, Error>func getIntAsResult(key: String, defValue: Int32) async -> Result<Int32, Error>func getLongAsResult(key: String, defValue: Int64) async -> Result<Int64, Error>func getFloatAsResult(key: String, defValue: Float) async -> Result<Float, Error>func getBooleanAsResult(key: String, defValue: Bool) async -> Result<Bool, Error>extension KvsKvsEditor { func apply() async -> Result<Bool, Error> }Example usage:
Task {
let result = await kvs.getStringAsResult(key: "username", defValue: "guest")
switch result {
case .success(let username):
// use username
print(username)
case .failure(let error):
// handle error
print(error)
}
let commit = await kvs.edit()
.putString(key: "username", value: "john")
.putBoolean(key: "is_premium", value: true)
.apply()
switch commit {
case .success:
// committed
break
case .failure(let error):
// handle commit error
print(error)
}
}Note: getAllAsResult() is currently available in the Kotlin extensions. An equivalent Swift helper is not exposed at this time; you can still call await kvs.getAll() directly if needed.
Thread Safety: All operations are thread-safe, but be mindful of the coroutine context when performing operations.
Memory Management: The storage is persistent, but avoid storing large amounts of data as it's not designed for large datasets.
Error Handling: Wrap storage operations in try-catch blocks when appropriate, especially when dealing with critical data.
Key Naming: Use a consistent naming convention for keys (e.g., user_pref_username, app_settings_theme).
Performance: For multiple operations, use the editor pattern to batch changes into a single atomic operation.
DataStore under the hood for modern, type-safe storageCopyright 2025 Santiago Mattiauda
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
KvsStorage is a Kotlin Multiplatform library that provides a simple, type-safe key-value storage solution. It's designed to work across multiple platforms including Android, iOS, and other Kotlin Multiplatform targets.
The library offers:
Add the following to your shared module's build.gradle.kts:
val kvsVersion = "1.0.0" // Check for the latest version
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.santimattius:kvs:$kvsVersion")
}
}
}// In your Application class or dependency injection module
private val kvs = Storage.kvs("user_preferences")// In your AppDelegate or dependency injection setup
private let kvs = Storage.shared.kvs(name: "user_preferences")You can also initialize an encrypted KVS on each platform:
// In your Application class or dependency injection module
private val kvs = Storage.encryptKvs(name = "user_preferences", key = "secret")// In your AppDelegate or dependency injection setup
private let kvs = Storage.shared.encryptKvs(name: "user_pref", key: "secret")// Using the editor pattern for multiple operations
kvs.edit()
.putString("username", "john_doe")
.putInt("user_age", 30)
.putBoolean("is_premium", true)
.commit() // Commits all changes atomically// Launch in a coroutine scope
lifecycleScope.launch {
val username = kvs.getString("username", "default_user")
val age = kvs.getInt("user_age", 0)
val isPremium = kvs.getBoolean("is_premium", false)
// Get all stored values
val allValues = kvs.getAll()
}// Remove a single key
kvs.edit()
.remove("username")
.commit()
// Clear all data
kvs.edit()
.clear()
.commit()Store and retrieve a single serializable object (e.g., a profile) as a document.
import kotlinx.serialization.Serializable
@Serializable
data class Profile(val username: String, val email: String)
val document = Storage.document("profile")
// val document = Storage.encryptDocument("profile", "secret")
val userProfile = Profile(username = "santimattius", email = "email@example.com")
document.put(userProfile)
val result: Profile? = document.get()struct Profile: Codable { let username: String; let email: String }
let document = Storage.shared.document(name: "profile")
// let document = Storage.shared.encryptDocument(name: "profile", secretKey: "secret")
let profile = Profile(username: "santimattius", email: "email@example.com")
try await document.put(value: profile)
let saved: Profile = try await document.get()For testing purposes, you can use InMemoryKvs which provides a volatile, in-memory implementation of the KVS interface. This is particularly useful for unit tests where you don't want to persist any data between test runs.
// In your test class (commonTest)
private val testKvs = Storage.inMemoryKvs("test_preferences")
@Test
fun `test kvs operations`() = runTest {
// Store data
testKvs.edit()
.putString("test_key", "test_value")
.commit()
// Retrieve data
val value = testKvs.getString("test_key", "default")
assertEquals("test_value", value)
// Clear data between tests
testKvs.edit().clear().commit()
}
// On iOS (Swift)
// private let testKvs = Storage.shared.inMemoryKvs(name: "test_preferences")Experimental: The TTL API is experimental and may change in future releases. Use @OptIn(ExperimentalKvsTtl::class) when using TTL in Kotlin.
Storage with TTL allows keys to expire after a duration. You can set a default TTL for the instance and optionally override it per key.
@OptIn(ExperimentalKvsTtl::class)
fun createCache() {
val ttl = object : Ttl {
override fun value() = kotlin.time.Duration.ofHours(1).inWholeMilliseconds
}
val cache = Storage.kvs("cache", ttl = ttl)
// Uses default TTL (1 hour)
cache.edit().putString("key1", "value1").commit()
// Override TTL for this key (30 minutes)
cache.edit().putString("key2", "value2", kotlin.time.Duration.ofMinutes(30)).commit()
// Expired keys return the default value; cleanup runs in batch via getAll() or CleanupJob
val value = cache.getString("key1", "default")
// Optional: background cleanup job (recommended for TTL storage)
cache.cleanupJob(kotlin.time.Duration.ofMinutes(10)).start(applicationScope)
}| API | Description |
|---|---|
Storage.kvs(name, ttl, encrypted) |
Creates a [KvsExtended] instance with optional default TTL and encryption. |
KvsExtended.edit().putString(key, value, duration) |
Overloads with duration set per-key TTL. |
KvsExtended.cleanupJob(interval) |
Returns a job that periodically removes expired keys on a background dispatcher. |
lifecycleScope.launch {
val hasUsername = kvs.contains("username")
}kvs.edit().apply {
// Multiple operations
putString("key1", "value1")
putInt("key2", 42)
remove("old_key")
}.commit() // All operations are atomicReact to value changes in keys using platform-native streams:
// Observe a single key
lifecycleScope.launch {
kvs.getStringAsStream("username", "default_user").collect { username ->
// React to username changes
}
}
// Other types
lifecycleScope.launch {
kvs.getIntAsStream("user_age", 0).collect { age -> }
}
lifecycleScope.launch {
kvs.getFloatAsStream("user_score", 0f).collect { score -> }
}
lifecycleScope.launch {
kvs.getBooleanAsStream("is_premium", false).collect { isPremium -> }
}
lifecycleScope.launch {
kvs.getLongAsStream("last_login", 0L).collect { lastLogin -> }
}
// Observe all values as a map
lifecycleScope.launch {
kvs.getAllAsStream().collect { allValues ->
// allValues: Map<String, Any?>
}
}// Observe a single key
Task {
for await username in kvs.getStringAsStream(name: "username", defaultValue: "default_user") {
// React to username changes
}
}
// Other types
Task {
for await age in kvs.getIntAsStream(name: "user_age", defaultValue: 0) {}
}
Task {
for await score in kvs.getFloatAsStream(name: "user_score", defaultValue: 0.0) {}
}
Task {
for await isPremium in kvs.getBooleanAsStream(name: "is_premium", defaultValue: false) {}
}
Task {
for await lastLogin in kvs.getLongAsStream(name: "last_login", defaultValue: 0) {}
}
// Observe all values as a dictionary
Task {
for await allValues in kvs.getAllAsStream() {
// allValues: [String: Any?]
}
}The library provides helper functions that wrap reads and edits into platform-native Result types to simplify error handling.
Available functions in shared/src/commonMain/kotlin/com/santimattius/kvs/KvsExtensions.kt:
suspend fun Kvs.getStringAsResult(key: String, defValue: String): Result<String>suspend fun Kvs.getIntAsResult(key: String, defValue: Int): Result<Int>suspend fun Kvs.getLongAsResult(key: String, defValue: Long): Result<Long>suspend fun Kvs.getFloatAsResult(key: String, defValue: Float): Result<Float>suspend fun Kvs.getBooleanAsResult(key: String, defValue: Boolean): Result<Boolean>suspend fun Kvs.getAllAsResult(): Result<Map<String, Any>>suspend fun Kvs.KvsEditor.apply(): Result<Boolean>Example usage:
lifecycleScope.launch {
val result = kvs.getStringAsResult("username", "guest")
result
.onSuccess { username -> /* use username */ }
.onFailure { error -> /* handle error */ }
val save = kvs.edit()
.putString("username", "john")
.putBoolean("is_premium", true)
.apply() // Result<Boolean>
save.onSuccess { /* committed */ }
.onFailure { e -> /* handle commit error */ }
}Note: these helpers execute in a non-cancellable context internally to ensure completion semantics and return failures as Result without throwing.
Available functions in shared/src/commonMain/swift/KvsExtensions.swift:
func getStringAsResult(key: String, defValue: String) async -> Result<String, Error>func getIntAsResult(key: String, defValue: Int32) async -> Result<Int32, Error>func getLongAsResult(key: String, defValue: Int64) async -> Result<Int64, Error>func getFloatAsResult(key: String, defValue: Float) async -> Result<Float, Error>func getBooleanAsResult(key: String, defValue: Bool) async -> Result<Bool, Error>extension KvsKvsEditor { func apply() async -> Result<Bool, Error> }Example usage:
Task {
let result = await kvs.getStringAsResult(key: "username", defValue: "guest")
switch result {
case .success(let username):
// use username
print(username)
case .failure(let error):
// handle error
print(error)
}
let commit = await kvs.edit()
.putString(key: "username", value: "john")
.putBoolean(key: "is_premium", value: true)
.apply()
switch commit {
case .success:
// committed
break
case .failure(let error):
// handle commit error
print(error)
}
}Note: getAllAsResult() is currently available in the Kotlin extensions. An equivalent Swift helper is not exposed at this time; you can still call await kvs.getAll() directly if needed.
Thread Safety: All operations are thread-safe, but be mindful of the coroutine context when performing operations.
Memory Management: The storage is persistent, but avoid storing large amounts of data as it's not designed for large datasets.
Error Handling: Wrap storage operations in try-catch blocks when appropriate, especially when dealing with critical data.
Key Naming: Use a consistent naming convention for keys (e.g., user_pref_username, app_settings_theme).
Performance: For multiple operations, use the editor pattern to batch changes into a single atomic operation.
DataStore under the hood for modern, type-safe storageCopyright 2025 Santiago Mattiauda
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.