Multiplatform Settings
repository·main·Indexed 25 days ago
https://github.com/russhwolf/multiplatform-settingsA Kotlin Multiplatform (KMP) library that enables common code to persist key-value data using platform-specific storage mechanisms. It provides a unified Settings interface with implementations for Android (SharedPreferences), iOS/macOS/tvOS/watchOS (NSUserDefaults, Keychain), JVM (Preferences, Properties), JS/WasmJS (Storage), and Windows (Registry). The library includes additional modules for serialization, Coroutine/Flow support, Jetpack DataStore integration, and in-memory testing via MapSettings.
What's inside multiplatform-settings
- Multiplatform Settings is a Kotlin library designed for Kotlin Multiplatform (KMP) applications. It provides a unified way for common code to persist key-value data across different platforms.
Manage multiple Settings instances with Factories
mainSome platforms provide a
Factoryclass to manage multiple namedSettingsobjects from common code. The factory must be injected from platform code, but once available in common code, you can create multiple instances with different names.// Android: Factory requires a Context val context: Context // ... val factory: Settings.Factory = SharedPreferencesSettings.Factory(context) // Most other platforms: Factory can be instantiated without parameters val factory: Settings.Factory = NSUserDefaultsSettings.Factory() // Usage in common code: val settings1: Settings = factory.create("my_first_settings") val settings2: Settings = factory.create("my_other_settings")Experimental APIs and Implementations
mainCertain APIs in this library are marked with annotations to warn users that they may break in the future:
@ExperimentalSettingsApi: Applied to APIs that are not yet stable.@ExperimentalSettingsImplementation: Applied to implementations that are not yet stable.@ExperimentalSerializationApi: Applied to the serialization module APIs.
Experimental Implementations
The following implementations are currently considered experimental:
KeychainSettingson Apple platforms.RegistrySettingson Windows.
Create a Settings instance using platform-specific delegates
mainTo interoperate with platform-specific code, you can instantiate
Settingsimplementations by passing their respective platform delegates. This is useful when using dependency injection to share the same data source between common and platform code.// Android: SharedPreferencesSettings wraps SharedPreferences val delegate: SharedPreferences // ... val settings: Settings = SharedPreferencesSettings(delegate) // iOS/macOS/tvOS/watchOS: NSUserDefaultsSettings wraps NSUserDefaults val delegate: NSUserDefaults // ... val settings: Settings = NSUserDefaultsSettings(delegate) // Apple platforms: KeychainSettings (experimental) val serviceName: String // ... val settings: Settings = KeychainSettings(serviceName) // JVM: PreferencesSettings wraps Preferences val delegate: Preferences // ... val settings: Settings = PreferencesSettings(delegate) // JVM: PropertiesSettings wraps Properties val delegate: Properties // ... val settings: Settings = PropertiesSettings(delegate) // JS/WasmJS: StorageSettings wraps Storage val delegate: Storage // ... val settings: Settings = StorageSettings(delegate) // Or use localStorage by default: val settings: Settings = StorageSettings() // Windows: RegistrySettings wraps Windows Registry val rootKey: String = "SOFTWARE\..." // Interpreted as subkey of HKEY_CURRENT_USER val settings: Settings = RegistrySettings(rootKey)Use the Serialization module for non-primitive data
mainThe
multiplatform-settings-serializationmodule allows you to save and retrieve@Serializableclasses using theSettingsstore as a format.Note: Using these APIs requires accepting both
@ExperimentalSettingsApiand@ExperimentalSerializationApiannotations.To use it, add the dependency:
implementation("com.russhwolf:multiplatform-settings-serialization:1.3.0")Key operations:
- Store: Use
encodeValue(serializer, key, value). - Retrieve: Use
decodeValue(serializer, key, defaultValue)ordecodeValueOrNull(serializer, key). - Remove: Use
removeValue(serializer, key)orremoveValue(serializer, key, ignorePartial = true)to remove serialized data. - Check existence: Use
containsValue(serializer, key). - Delegates: Use
serializedValue(...)ornullableSerializedValue(...)for property delegation. - Implicit Serializers: You can omit the serializer parameter (e.g.,
encodeValue("key", value)), but these will throw if the class is not serializable.
@Serializable class SomeClass(val someProperty: String, anotherProperty: Int) val someClass: SomeClass val settings: Settings // Store values for the properties of someClass in settings settings.encodeValue(SomeClass.serializer(), "key", someClass) // Create a new instance of SomeClass based on the data in settings val newInstance: SomeClass = settings.decodeValue(SomeClass.serializer(), "key", defaultValue) val nullableNewInstance: SomeClass = settings.decodeValueOrNull(SomeClass.serializer(), "key") // To remove a serialized value, use removeValue() rather than remove() settings.removeValue(SomeClass.serializer(), "key") // Don't remove if not all expected data is preset settings.removeValue(SomeClass.serializer(), "key", ignorePartial = true) // To check for the existence of a serialized value, use containsValue() rather than contains() val isPresent = settings.containsValue(SomeClass.serializer(), "key") // Delegate API val someClass: SomeClass by settings.serializedValue(SomeClass.serializer(), "someClass", defaultValue) val nullableSomeClass: SomeClass? by settings.nullableSerializedValue(SomeClass.serializer(), "someClass") // Implicit serializer variants settings.encodeValue("key", someClass) val newInstance: SomeClass = settings.decodeValue("key", defaultValue) val nullableNewInstance: SomeClass = settings.decodeValueOrNull("key")- Store: Use
Test settings using MapSettings
mainFor unit testing code that depends onSettings, use themultiplatform-settings-testdependency. It providesMapSettings, which is an in-memoryMutableMapimplementation available on all platforms.Use Coroutine and Flow APIs
mainThe
multiplatform-settings-coroutinesmodule provides Flow-based extensions forObservableSettings.Dependency:
implementation("com.russhwolf:multiplatform-settings-coroutines:1.3.0")Available Interfaces
ObservableSettings: Provides Flow extensions for all types (e.g.,getIntFlow,getIntOrNullFlow).StateFlowextensions: Requires aCoroutineScopeto provideStateFlow(e.g.,getIntStateFlow).SuspendSettings: ASettings-like interface where all functions are markedsuspend.FlowSettings: ExtendsSuspendSettingsto include Flow-based getters.
Interface Conversion
You can convert between these interfaces to select a primary interface for your common code:
settings.toSuspendSettings()observableSettings.toFlowSettings()suspendSettings.toBlockingSettings()flowSettings.toBlockingObservableSettings()
// Coroutine Flow extensions val observableSettings: ObservableSettings val flow: Flow<Int> by observableSettings.getIntFlow("key", defaultValue) val nullableFlow: Flow<Int?> by observableSettings.getIntOrNullFlow("key") // StateFlow extensions val coroutineScope: CoroutineScope val stateFlow: StateFlow<Int> by observableSettings.getIntStateFlow("key", defaultValue) val nullableStateFlow: StateFlow<Int?> by observableSettings.getIntOrNullStateFlow("key") // Suspend and Flow interfaces val suspendSettings: SuspendSettings val a: Int = suspendSettings.getInt("key") val flowSettings: FlowSettings val flow: Flow<Int> = flowSettings.getIntFlow("key") // Conversions val settings: Settings val suspendSettings: SuspendSettings = settings.toSuspendSettings() val observableSettings: ObservableSettings val flowSettings: FlowSettings = observableSettings.toFlowSettings() val blockingSettings: Settings = suspendSettings.toBlockingSettings() val blockingSettings: ObservableSettings = flowSettings.toBlockingObservableSettings()Use the no-arg module for easy common-code instantiation
mainIf you want to create a
Settingsinstance directly from common code without passing platform-specific dependencies, use themultiplatform-settings-no-argmodule. This provides aSettings()factory function that selects a sensible default for each platform (e.g.,NSUserDefaults.standardUserDefaultson Apple,localStorageon JS,SharedPreferenceson Android viaandroidx-startup).Note: This module is intended for getting started. It does not support encrypted implementations or easy substitution for testing. You cannot call
Settings()in Android unit tests because it requires aContextprovided byandroidx-startup.Use Jetpack DataStore implementation
mainThe
multiplatform-settings-datastoremodule provides aFlowSettingsimplementation based on Jetpack DataStore. Since version 1.2.0, this is available on all platforms where DataStore is available, not just Android/JVM.Dependency:
implementation("com.russhwolf:multiplatform-settings-datastore:1.3.0")Usage Patterns
Using FlowSettings (for platforms with listener support)
In common code, you can expect a
FlowSettingsinstance and provide platform-specific implementations:// Common expect val settings: FlowSettings // Android actual val settings: FlowSettings = DataStoreSettings(dataStore) // iOS actual val settings: FlowSettings = NSUserDefaultsSettings(…).toFlowSettings()Using SuspendSettings (for platforms without listener support)
If you are targeting platforms without listener support (like JS), use
SuspendSettingsinstead:// Common expect val settings: SuspendSettings // Android actual val settings: SuspendSettings = DataStoreSettings(dataStore) // iOS actual val settings: SuspendSettings = NSUserDefaultsSettings(…).toSuspendSettings() // JS actual val settings: SuspendSettings = StorageSettings().toSuspendSettings()Install Multiplatform Settings
mainTo use Multiplatform Settings, ensure
mavenCentral()is in your repositories. Add the dependency to yourcommonMainsource-set dependencies in your Gradle configuration.repositories { mavenCentral() } // In your build.gradle.kts commonMain { dependencies { implementation("com.russhwolf:multiplatform-settings:1.3.0") } }Use the Settings API to store and retrieve values
mainThe
SettingsAPI provides methods for storing, retrieving, and querying key-value pairs. You can use explicitputXXX/getXXXmethods or operator shortcuts.// Storing values settings.putInt("key", 3) settings["key"] = 3 // Retrieving values with defaults val a: Int = settings.getInt("key") val b: Int = settings.getInt("key", defaultValue = -1) val c: Int = settings["key", -1] // Retrieving nullable values (returns null if key is missing) val a: Int? = settings.getIntOrNull("key") val b: Int? = settings["key"] // Existence and removal val exists: Boolean = settings.hasKey("key") val existsShortcut: Boolean = "key" in settings settings.remove("key") settings -= "key" settings["key"] = null // Removes the key // Bulk operations settings.clear() val keys: Set<String> = settings.keys val size: Int = settings.sizeMake Settings observable with makeObservable()
mainThe experimental
multiplatform-settings-make-observablemodule provides an extension functionSettings.makeObservable()in common code. This converts a standardSettingsinstance into anObservableSettingsinstance by wiring in callbacks manually.Advantages: Enables observability on platforms that do not have native observability methods. Disadvantages: Updates are only delivered to the same instance where the changes were made.
val settings: Settings val observableSettings: ObservableSettings = settings.makeObservable()