
Fast MMKV-compatible key-value store offering a unified, type-safe API, MMKV binary format, drop-in preferences adapters, null-on-miss getters, synchronous C++ core and no reflection or annotation processors.
A fast, MMKV-compatible key-value store for Kotlin Multiplatform — Android and iOS from a single API.
QuickStore is a thin Kotlin Multiplatform wrapper over a C++ MMKV-compatible storage engine. It exposes a unified, type-safe API for persisting primitive values on Android (AAR + JNI) and iOS (XCFramework). The C++ core is frozen and battle-tested; QuickStore adds the Kotlin and Swift layers.
null on miss (no sentinel values)SharedPreferences adapter for Android migration| Platform | Minimum version |
|---|---|
| Android | minSdk 24 (Android 7.0) |
| iOS | iOS 13+ |
Add the dependency to your module's build.gradle.kts:
dependencies {
implementation("io.github.santimattius:quickstore:0.1.0-alpha01")
}Make sure mavenCentral() is in your repository list.
Native bridge: QuickStore ships its JNI layer as a separate artifact (
quickstore-native). Gradle resolves it automatically as a transitive dependency — you do not need to declare it explicitly. Both artifacts are versioned together and published simultaneously.
In Xcode: File > Add Package Dependencies, enter the URL:
https://github.com/santimattius/quick-store
Select the QuickStore product and add it to your target.
Alternatively, add to your Package.swift:
dependencies: [
.package(url: "https://github.com/santimattius/quick-store", from: "0.1.0-alpha01")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["QuickStore"]
)
]Note: On the
mainbranch,Package.swiftcarries aREPLACE_WITH_CHECKSUMplaceholder and is NOT directly consumable. The release workflow (publish.yml) computes the real checksum and the release URL on eachv*.*.*tag, then commits the resolvedPackage.swiftback tomain. Always depend on a tagged version (from: "x.y.z"), never onbranch: "main".
import com.quickstore.QuickStore
import com.quickstore.putString
import com.quickstore.getString
// Open a store — mmkvId is the store name, rootDir is the storage directory
val store = QuickStore(
mmkvId = "app_prefs",
rootDir = context.filesDir.absolutePath
)
// Write
store.putString("username", "santiago")
store.setLong("login_count", 42L)
store.setBool("onboarding_done", true)
// Read
val username: String? = store.getString("username")
val count: Long? = store.getLong("login_count")
val done: Boolean? = store.getBool("onboarding_done")
// Default-value overloads
val safeUsername: String = store.getString("username", "guest")
val safeCount: Long = store.getLong("login_count", 0L)
// Cleanup
store.close()import QuickStore
// Open a store
let store = QuickStore(
mmkvId: "app_prefs",
rootDir: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path
)
// Write
_ = store.setLong(key: "login_count", value: 42)
_ = store.setBool(key: "onboarding_done", value: true)
// Read (returns Optional)
let count: Int64? = store.getLong(key: "login_count")
let done: Bool? = store.getBool(key: "onboarding_done")
// Cleanup
store.close()iOS — Kotlin extension functions: The Kotlin extensions in
QuickStoreExtensions.kt(putString,getInt, default-value overloads, …) are exposed to Objective-C/Swift as top-level functions on the generatedQuickStoreExtensionsKtclass, with mangled selectors such asQuickStoreExtensionsKt.putStringForStore_key_value_. The exact selector names are generated by Kotlin/Native and may shift across versions — always consult the generatedQuickStore.hin the XCFramework for the authoritative signatures rather than hardcoding mangled names.
QuickStoreSharedPreferences is a drop-in adapter that implements the SharedPreferences interface backed by a QuickStore instance. Swap one line:
// Before
val prefs: SharedPreferences = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
// After — same interface, faster storage
val store = QuickStore(mmkvId = "app_prefs", rootDir = context.filesDir.absolutePath)
val prefs: SharedPreferences = QuickStoreSharedPreferences(store)All existing getString, getInt, getLong, getFloat, getBoolean, contains, edit(), commit(), and apply() calls continue to work without change.
See Known Limitations for unsupported operations.
Replace UserDefaults calls with the equivalent QuickStore primitives:
// UserDefaults
UserDefaults.standard.set("santiago", forKey: "username")
let name = UserDefaults.standard.string(forKey: "username")
// QuickStore — equivalent
_ = store.putString(key: "username", value: "santiago") // extension
let name: String? = store.getString(key: "username") // extensionNumeric migration:
// UserDefaults
UserDefaults.standard.set(42, forKey: "count")
// QuickStore
_ = store.setLong(key: "count", value: 42)
let count = store.getLong(key: "count") // Int64?| Signature | Platform | Description |
|---|---|---|
QuickStore(mmkvId: String, rootDir: String) |
Both | Opens (or creates) a store with the given ID in rootDir. Throws on failure. |
| Method | Platform | Description |
|---|---|---|
setBool(key: String, value: Boolean): Boolean |
Both | Stores a boolean value. |
setLong(key: String, value: Long): Boolean |
Both | Stores a 64-bit integer. |
setDouble(key: String, value: Double): Boolean |
Both | Stores a 64-bit float. |
setBytes(key: String, value: ByteArray): Boolean |
Both | Stores a raw byte array. |
| Method | Platform | Description |
|---|---|---|
getBool(key: String): Boolean? |
Both | Retrieves a boolean, or null if absent. |
getLong(key: String): Long? |
Both | Retrieves a 64-bit integer, or null if absent. |
getDouble(key: String): Double? |
Both | Retrieves a 64-bit float, or null if absent. |
getBytes(key: String): ByteArray? |
Both | Retrieves a byte array, or null if absent. |
| Method | Description |
|---|---|
putString(key: String, value: String): Boolean |
Stores a UTF-8 string (encoded as bytes). |
getString(key: String): String? |
Retrieves a UTF-8 string, or null if absent. |
getString(key: String, default: String): String |
Retrieves a string, falling back to default. |
putInt(key: String, value: Int): Boolean |
Stores an integer (promoted to Long). |
getInt(key: String): Int? |
Retrieves an integer, or null if absent. |
getInt(key: String, default: Int): Int |
Retrieves an integer, falling back to default. |
putFloat(key: String, value: Float): Boolean |
Stores a float (promoted to Double). |
getFloat(key: String): Float? |
Retrieves a float, or null if absent. |
getFloat(key: String, default: Float): Float |
Retrieves a float, falling back to default. |
getBool(key: String, default: Boolean): Boolean |
Retrieves a boolean, falling back to default. |
getLong(key: String, default: Long): Long |
Retrieves a Long, falling back to default. |
getDouble(key: String, default: Double): Double |
Retrieves a Double, falling back to default. |
| Method | Description |
|---|---|
contains(key: String): Boolean |
Returns true if the key exists in the store. |
remove(key: String): Boolean |
Removes the entry for the given key. Returns true on success. |
count(): Long |
Returns the total number of entries in the store. |
allKeys(): List<String> |
Returns all keys currently stored. |
trim() |
Reclaims unused storage space (equivalent to MMKV trim). |
clear() |
Removes all entries from the store. |
close() |
Closes the store and releases native resources. |
Requires
@OptIn(ExperimentalQuickStoreApi::class). See Experimental API stability.
| Method | Description |
|---|---|
batchGetLongs(keys: List<String>): Map<String, Long?> |
Reads many longs in one round-trip; absent keys map to null. |
batchGetBools(keys: List<String>): Map<String, Boolean?> |
Reads many booleans in one round-trip. |
batchGetDoubles(keys: List<String>): Map<String, Double?> |
Reads many doubles in one round-trip. |
batchSetLongs(pairs: Map<String, Long>) |
Writes many longs in one round-trip. |
batchSetBools(pairs: Map<String, Boolean>) |
Writes many booleans in one round-trip. |
batchSetDoubles(pairs: Map<String, Double>) |
Writes many doubles in one round-trip. |
@OptIn(ExperimentalQuickStoreApi::class)
fun example(store: QuickStore) {
// Write several counters in a single native call
store.batchSetLongs(mapOf("views" to 1_000L, "clicks" to 42L, "shares" to 7L))
// Read them all back in one round-trip
val counts: Map<String, Long?> = store.batchGetLongs(listOf("views", "clicks", "shares"))
println(counts) // {views=1000, clicks=42, shares=7}
}The @ExperimentalQuickStoreApi marker indicates that the shape of an API (its name, signature, or existence) may change in any minor release before 1.0 WITHOUT a deprecation cycle. The underlying storage behavior is stable and MMKV-compatible — only the Kotlin surface is provisional.
To suppress the compiler warning, opt in at the call site:
@OptIn(ExperimentalQuickStoreApi::class)
fun myFunction() { /* use batch APIs here */ }Or propagate the opt-in to a whole class or file:
@file:OptIn(ExperimentalQuickStoreApi::class)| Class / Method | Description |
|---|---|
QuickStoreSharedPreferences(store: QuickStore) |
Wraps a QuickStore as a SharedPreferences. Drop-in replacement for getSharedPreferences(...). |
StringSet support: getStringSet / putStringSet throw UnsupportedOperationException. The C ABI has no multi-value encoding. Use putString with JSON serialization if you need sets.registerOnSharedPreferenceChangeListener / unregisterOnSharedPreferenceChangeListener throw UnsupportedOperationException. The frozen C++ core has no notification hook.commit() == apply(): Both are synchronous. commit() always returns true. There is no deferred write queue.QuickStoreFactory does not exist: The library does not ship a factory object. Instantiate QuickStore(mmkvId, rootDir) directly.io.github.santimattius:quickstore and io.github.santimattius:quickstore-native at the same version. The main artifact's POM declares quickstore-native as a transitive dependency; publishing only one will cause a resolution failure for consumers.QuickStore is a Kotlin Multiplatform wrapper over a frozen C++ MMKV-compatible storage engine. On Android it ships as an AAR with a JNI bridge (libquickstore_jni.so, packaged in the quickstore-native artifact); on iOS it ships as an XCFramework built via Kotlin/Native cinterop. The Kotlin layer uses expect/actual declarations so commonMain holds the API contract and each platform target (androidMain, iosMain) provides the platform implementation.
Why it is fast: The C++ core uses an mmap-backed memory region and an append-only write-log. Writes append a type-tagged record to the end of the log; reads are served from the mapped memory region without a system call. Periodic calls to trim() compact the log by rewriting only live entries, reclaiming any space accumulated from overwritten or removed keys.
Type model: Every value carries a type tag in the core. A getter of the wrong type (e.g., calling getLong on a key that stores a boolean) returns null rather than a corrupt value. This is the source of the nullability contract documented throughout the API.
Two-artifact publishing: The library publishes as two Maven coordinates — quickstore (the Kotlin layer) and quickstore-native (the Android JNI AAR). Both are versioned together and must be published simultaneously. See Known Limitations for details.
For tests, a FakeQuickStore in-memory double exists in the source tree under commonTest. It is not part of the published artifact — copy it into your own test source set if you want it.
Copyright 2026 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
https://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.
A fast, MMKV-compatible key-value store for Kotlin Multiplatform — Android and iOS from a single API.
QuickStore is a thin Kotlin Multiplatform wrapper over a C++ MMKV-compatible storage engine. It exposes a unified, type-safe API for persisting primitive values on Android (AAR + JNI) and iOS (XCFramework). The C++ core is frozen and battle-tested; QuickStore adds the Kotlin and Swift layers.
null on miss (no sentinel values)SharedPreferences adapter for Android migration| Platform | Minimum version |
|---|---|
| Android | minSdk 24 (Android 7.0) |
| iOS | iOS 13+ |
Add the dependency to your module's build.gradle.kts:
dependencies {
implementation("io.github.santimattius:quickstore:0.1.0-alpha01")
}Make sure mavenCentral() is in your repository list.
Native bridge: QuickStore ships its JNI layer as a separate artifact (
quickstore-native). Gradle resolves it automatically as a transitive dependency — you do not need to declare it explicitly. Both artifacts are versioned together and published simultaneously.
In Xcode: File > Add Package Dependencies, enter the URL:
https://github.com/santimattius/quick-store
Select the QuickStore product and add it to your target.
Alternatively, add to your Package.swift:
dependencies: [
.package(url: "https://github.com/santimattius/quick-store", from: "0.1.0-alpha01")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["QuickStore"]
)
]Note: On the
mainbranch,Package.swiftcarries aREPLACE_WITH_CHECKSUMplaceholder and is NOT directly consumable. The release workflow (publish.yml) computes the real checksum and the release URL on eachv*.*.*tag, then commits the resolvedPackage.swiftback tomain. Always depend on a tagged version (from: "x.y.z"), never onbranch: "main".
import com.quickstore.QuickStore
import com.quickstore.putString
import com.quickstore.getString
// Open a store — mmkvId is the store name, rootDir is the storage directory
val store = QuickStore(
mmkvId = "app_prefs",
rootDir = context.filesDir.absolutePath
)
// Write
store.putString("username", "santiago")
store.setLong("login_count", 42L)
store.setBool("onboarding_done", true)
// Read
val username: String? = store.getString("username")
val count: Long? = store.getLong("login_count")
val done: Boolean? = store.getBool("onboarding_done")
// Default-value overloads
val safeUsername: String = store.getString("username", "guest")
val safeCount: Long = store.getLong("login_count", 0L)
// Cleanup
store.close()import QuickStore
// Open a store
let store = QuickStore(
mmkvId: "app_prefs",
rootDir: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path
)
// Write
_ = store.setLong(key: "login_count", value: 42)
_ = store.setBool(key: "onboarding_done", value: true)
// Read (returns Optional)
let count: Int64? = store.getLong(key: "login_count")
let done: Bool? = store.getBool(key: "onboarding_done")
// Cleanup
store.close()iOS — Kotlin extension functions: The Kotlin extensions in
QuickStoreExtensions.kt(putString,getInt, default-value overloads, …) are exposed to Objective-C/Swift as top-level functions on the generatedQuickStoreExtensionsKtclass, with mangled selectors such asQuickStoreExtensionsKt.putStringForStore_key_value_. The exact selector names are generated by Kotlin/Native and may shift across versions — always consult the generatedQuickStore.hin the XCFramework for the authoritative signatures rather than hardcoding mangled names.
QuickStoreSharedPreferences is a drop-in adapter that implements the SharedPreferences interface backed by a QuickStore instance. Swap one line:
// Before
val prefs: SharedPreferences = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
// After — same interface, faster storage
val store = QuickStore(mmkvId = "app_prefs", rootDir = context.filesDir.absolutePath)
val prefs: SharedPreferences = QuickStoreSharedPreferences(store)All existing getString, getInt, getLong, getFloat, getBoolean, contains, edit(), commit(), and apply() calls continue to work without change.
See Known Limitations for unsupported operations.
Replace UserDefaults calls with the equivalent QuickStore primitives:
// UserDefaults
UserDefaults.standard.set("santiago", forKey: "username")
let name = UserDefaults.standard.string(forKey: "username")
// QuickStore — equivalent
_ = store.putString(key: "username", value: "santiago") // extension
let name: String? = store.getString(key: "username") // extensionNumeric migration:
// UserDefaults
UserDefaults.standard.set(42, forKey: "count")
// QuickStore
_ = store.setLong(key: "count", value: 42)
let count = store.getLong(key: "count") // Int64?| Signature | Platform | Description |
|---|---|---|
QuickStore(mmkvId: String, rootDir: String) |
Both | Opens (or creates) a store with the given ID in rootDir. Throws on failure. |
| Method | Platform | Description |
|---|---|---|
setBool(key: String, value: Boolean): Boolean |
Both | Stores a boolean value. |
setLong(key: String, value: Long): Boolean |
Both | Stores a 64-bit integer. |
setDouble(key: String, value: Double): Boolean |
Both | Stores a 64-bit float. |
setBytes(key: String, value: ByteArray): Boolean |
Both | Stores a raw byte array. |
| Method | Platform | Description |
|---|---|---|
getBool(key: String): Boolean? |
Both | Retrieves a boolean, or null if absent. |
getLong(key: String): Long? |
Both | Retrieves a 64-bit integer, or null if absent. |
getDouble(key: String): Double? |
Both | Retrieves a 64-bit float, or null if absent. |
getBytes(key: String): ByteArray? |
Both | Retrieves a byte array, or null if absent. |
| Method | Description |
|---|---|
putString(key: String, value: String): Boolean |
Stores a UTF-8 string (encoded as bytes). |
getString(key: String): String? |
Retrieves a UTF-8 string, or null if absent. |
getString(key: String, default: String): String |
Retrieves a string, falling back to default. |
putInt(key: String, value: Int): Boolean |
Stores an integer (promoted to Long). |
getInt(key: String): Int? |
Retrieves an integer, or null if absent. |
getInt(key: String, default: Int): Int |
Retrieves an integer, falling back to default. |
putFloat(key: String, value: Float): Boolean |
Stores a float (promoted to Double). |
getFloat(key: String): Float? |
Retrieves a float, or null if absent. |
getFloat(key: String, default: Float): Float |
Retrieves a float, falling back to default. |
getBool(key: String, default: Boolean): Boolean |
Retrieves a boolean, falling back to default. |
getLong(key: String, default: Long): Long |
Retrieves a Long, falling back to default. |
getDouble(key: String, default: Double): Double |
Retrieves a Double, falling back to default. |
| Method | Description |
|---|---|
contains(key: String): Boolean |
Returns true if the key exists in the store. |
remove(key: String): Boolean |
Removes the entry for the given key. Returns true on success. |
count(): Long |
Returns the total number of entries in the store. |
allKeys(): List<String> |
Returns all keys currently stored. |
trim() |
Reclaims unused storage space (equivalent to MMKV trim). |
clear() |
Removes all entries from the store. |
close() |
Closes the store and releases native resources. |
Requires
@OptIn(ExperimentalQuickStoreApi::class). See Experimental API stability.
| Method | Description |
|---|---|
batchGetLongs(keys: List<String>): Map<String, Long?> |
Reads many longs in one round-trip; absent keys map to null. |
batchGetBools(keys: List<String>): Map<String, Boolean?> |
Reads many booleans in one round-trip. |
batchGetDoubles(keys: List<String>): Map<String, Double?> |
Reads many doubles in one round-trip. |
batchSetLongs(pairs: Map<String, Long>) |
Writes many longs in one round-trip. |
batchSetBools(pairs: Map<String, Boolean>) |
Writes many booleans in one round-trip. |
batchSetDoubles(pairs: Map<String, Double>) |
Writes many doubles in one round-trip. |
@OptIn(ExperimentalQuickStoreApi::class)
fun example(store: QuickStore) {
// Write several counters in a single native call
store.batchSetLongs(mapOf("views" to 1_000L, "clicks" to 42L, "shares" to 7L))
// Read them all back in one round-trip
val counts: Map<String, Long?> = store.batchGetLongs(listOf("views", "clicks", "shares"))
println(counts) // {views=1000, clicks=42, shares=7}
}The @ExperimentalQuickStoreApi marker indicates that the shape of an API (its name, signature, or existence) may change in any minor release before 1.0 WITHOUT a deprecation cycle. The underlying storage behavior is stable and MMKV-compatible — only the Kotlin surface is provisional.
To suppress the compiler warning, opt in at the call site:
@OptIn(ExperimentalQuickStoreApi::class)
fun myFunction() { /* use batch APIs here */ }Or propagate the opt-in to a whole class or file:
@file:OptIn(ExperimentalQuickStoreApi::class)| Class / Method | Description |
|---|---|
QuickStoreSharedPreferences(store: QuickStore) |
Wraps a QuickStore as a SharedPreferences. Drop-in replacement for getSharedPreferences(...). |
StringSet support: getStringSet / putStringSet throw UnsupportedOperationException. The C ABI has no multi-value encoding. Use putString with JSON serialization if you need sets.registerOnSharedPreferenceChangeListener / unregisterOnSharedPreferenceChangeListener throw UnsupportedOperationException. The frozen C++ core has no notification hook.commit() == apply(): Both are synchronous. commit() always returns true. There is no deferred write queue.QuickStoreFactory does not exist: The library does not ship a factory object. Instantiate QuickStore(mmkvId, rootDir) directly.io.github.santimattius:quickstore and io.github.santimattius:quickstore-native at the same version. The main artifact's POM declares quickstore-native as a transitive dependency; publishing only one will cause a resolution failure for consumers.QuickStore is a Kotlin Multiplatform wrapper over a frozen C++ MMKV-compatible storage engine. On Android it ships as an AAR with a JNI bridge (libquickstore_jni.so, packaged in the quickstore-native artifact); on iOS it ships as an XCFramework built via Kotlin/Native cinterop. The Kotlin layer uses expect/actual declarations so commonMain holds the API contract and each platform target (androidMain, iosMain) provides the platform implementation.
Why it is fast: The C++ core uses an mmap-backed memory region and an append-only write-log. Writes append a type-tagged record to the end of the log; reads are served from the mapped memory region without a system call. Periodic calls to trim() compact the log by rewriting only live entries, reclaiming any space accumulated from overwritten or removed keys.
Type model: Every value carries a type tag in the core. A getter of the wrong type (e.g., calling getLong on a key that stores a boolean) returns null rather than a corrupt value. This is the source of the nullability contract documented throughout the API.
Two-artifact publishing: The library publishes as two Maven coordinates — quickstore (the Kotlin layer) and quickstore-native (the Android JNI AAR). Both are versioned together and must be published simultaneously. See Known Limitations for details.
For tests, a FakeQuickStore in-memory double exists in the source tree under commonTest. It is not part of the published artifact — copy it into your own test source set if you want it.
Copyright 2026 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
https://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.