Skip to content

Repository files navigation

CodeFactorMaven Central

Settings Multiplatform

settings-multiplatform provides a type-safe, multiplatform abstraction over AndroidX DataStore, letting you define preferences as objects (rather than string keys), and enabling encryption.

Note

Settings Multiplatform now support Encryption in version 2.3.0+

🔍 What it solves

Working with DataStore typically means you operate with string keys and primitive types, which is error-prone and lacks compile-time safety. When you migrate logic to a multiplatform structure (Android + iOS / Kotlin Multiplatform), managing platform-specific preferences becomes cumbersome.

settings-multiplatform solves these issues by:

  • Exposing preference definitions as typed objects (e.g. stringPreference, intPreference) instead of raw string keys
  • Handling both Android and iOS usage through a shared API
  • Supporting encrypted preferences on supported platforms

🛠 Key Features

  • ✅ Type safety: No more string key typos — you reference Preferences.preferenceString instead of "preference_string"
  • ✅ Simple usage API, same interface across platforms
  • ✅ Secure storage layer for sensitive settings on Android & iOS
    • Android: AES-256-GCM where supported, with fallback to AES-GCM, using Android Keystore-backed keys
    • iOS: Secure storage via Keychain

Dependency

Add the library to your module build.gradle

dependencies {
implementation 'de.charlex.settings:settings-datastore:<version>'
}

Usage

Declaring Preferences

object Preferences {
val preferenceInt = intPreference("preference_int", 1)
val preferenceString = stringPreference("preference_string", "default")
val preferenceFloat = floatPreference("preference_float", 1.1f)
val preferenceLong = longPreference("preference_long", 1L)
val preferenceBoolean = boolenPreference("preference_boolean", true)
}
object EncryptedPreferences {
val encryptedPreferenceInt = encryptedIntPreference("encrypted_preference_int", 1)
val encryptedPreferenceString = encryptedStringPreference("encrypted_preference_string", "default")
val encryptedPreferenceFloat = encryptedFloatPreference("encrypted_preference_float", 1.1f)
val encryptedPreferenceLong = encryptedLongPreference("encrypted_preference_long", 1L)
val encryptedPreferenceBoolean = encryptedBoolenPreference("encrypted_preference_boolean", true)
}

Using Preferences

src@android

val settingsDatastore =SettingsDataStore.create(
context = context,
name ="multiplatform-datastore.preferences_pb",
encryptedStore = {
AESEncryptedStore(it)
}
)

src@ios

val settingsDatastore =SettingsDataStore.create(
name ="multiplatform-datastore.preferences_pb",
// Applies to every keychain item of this store (add, update, read and delete)
keychainOptions =KeychainOptions(
accessibility =KeychainAccessibility.AfterFirstUnlockThisDeviceOnly
),
encryptedStore = {
KeychainStore(
dataStore = it,
keychain =Keychain(
appGroup ="group.xxx",
service =NSBundle.mainBundle.bundleIdentifier,
defaultOptions =KeychainOptions(
accessibility =KeychainAccessibility.AfterFirstUnlockThisDeviceOnly
)
)
)
}
)

Keychain options (iOS)

The Security framework uses the same dictionary shape for very different purposes, which makes it easy to configure a keychain item incorrectly:

CallDictionaryContains
SecItemAddattributesidentity + item attributes + value
SecItemUpdatequeryidentity only
SecItemUpdateattributesToUpdateitem attributes + value
SecItemCopyMatchingqueryidentity + return/match options
SecItemDeletequeryidentity

An attribute such as kSecAttrAccessible is an item attribute. If it is only written on SecItemAdd, an already existing item keeps its old protection class forever, because SecItemUpdate never receives it. If it is written into a search dictionary instead, every lookup starts to fail with errSecItemNotFound.

KeychainOptions separates identity from item attributes and puts every entry into the correct dictionary:

KeychainOptions(
// identity / search attributes – used by add, update, read and delete
baseQueryItems =listOf(kSecAttrSynchronizable to kCFBooleanFalse),
// item attributes – written on SecItemAdd AND on SecItemUpdate
itemAttributes =listOf(kSecAttrLabel to "My App Token"),
// convenience for kSecAttrAccessible (also written on add AND update)
accessibility =KeychainAccessibility.AfterFirstUnlockThisDeviceOnly,
// call specific escape hatches
addQueryItems = emptyList(),
updateQueryItems = emptyList(),
updateAttributes = emptyList(),
readQueryItems = emptyList(),
deleteQueryItems = emptyList(),
)

Options can be configured on three levels and are merged in this order (later wins):

  1. Keychain(defaultOptions = …) / SettingsDataStore.create(keychainOptions = …) – whole store
  2. KeychainStore(defaultOptions = …)
  3. SystemOptions of a single preference
val token = encryptedStringPreference(
name ="token",
defaultValue ="",
options =SystemOptions(accessibility =KeychainAccessibility.WhenPasscodeSetThisDeviceOnly)
)

Migration note:kSecAttrAccessible entries that were previously passed via keychainAddQueryItems (or keychainBaseQueryItems) keep working – protection attributes are always detected and routed to the add and update dictionaries, and they are never used as search attributes. applyUpdatableAttributesOnUpdate = false only disables forwarding of the remaining updatable attributes (e.g. kSecAttrLabel).

Usage

//Readval exampleString:Flow<String> = settingsDatastore.get(Preferences.PreferenceString)
val encryptedExampleString:Flow<String> = settingsDatastore.get(EncryptedPreferences.encryptedPreferenceString)
//Write
coroutineScope.launch {
settings.put(Preferences.preferenceString, "my value")
settings.put(EncryptedPreferences.encryptedPreferenceString, "shoulb be encrypted")
}

License

Copyright 2024 Alexander Karkossa
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.

About

A Kotlin Multiplatform wrapper for AndroidX DataStore with type-safe preferences and optional encryption support.

Topics

Resources

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages