Multiplatform Settings

repository·main·Indexed 25 days ago

https://github.com/russhwolf/multiplatform-settings

A 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.

Tokens
4.2K
Snippets
7
Records
15
Agent score
31%

What's inside multiplatform-settings

  1. Manage multiple Settings instances with Factories

    main

    Some platforms provide a Factory class to manage multiple named Settings objects 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")
  2. Experimental APIs and Implementations

    main

    Certain 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:

    • KeychainSettings on Apple platforms.
    • RegistrySettings on Windows.
  3. Create a Settings instance using platform-specific delegates

    main

    To interoperate with platform-specific code, you can instantiate Settings implementations 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)
  4. Use the Serialization module for non-primitive data

    main

    The multiplatform-settings-serialization module allows you to save and retrieve @Serializable classes using the Settings store as a format.

    Note: Using these APIs requires accepting both @ExperimentalSettingsApi and @ExperimentalSerializationApi annotations.

    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) or decodeValueOrNull(serializer, key).
    • Remove: Use removeValue(serializer, key) or removeValue(serializer, key, ignorePartial = true) to remove serialized data.
    • Check existence: Use containsValue(serializer, key).
    • Delegates: Use serializedValue(...) or nullableSerializedValue(...) 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")
  5. Use Coroutine and Flow APIs

    main

    The multiplatform-settings-coroutines module provides Flow-based extensions for ObservableSettings.

    Dependency:

    implementation("com.russhwolf:multiplatform-settings-coroutines:1.3.0")

    Available Interfaces

    • ObservableSettings: Provides Flow extensions for all types (e.g., getIntFlow, getIntOrNullFlow).
    • StateFlow extensions: Requires a CoroutineScope to provide StateFlow (e.g., getIntStateFlow).
    • SuspendSettings: A Settings-like interface where all functions are marked suspend.
    • FlowSettings: Extends SuspendSettings to 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()
  6. Use the no-arg module for easy common-code instantiation

    main

    If you want to create a Settings instance directly from common code without passing platform-specific dependencies, use the multiplatform-settings-no-arg module. This provides a Settings() factory function that selects a sensible default for each platform (e.g., NSUserDefaults.standardUserDefaults on Apple, localStorage on JS, SharedPreferences on Android via androidx-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 a Context provided by androidx-startup.

  7. Use Jetpack DataStore implementation

    main

    The multiplatform-settings-datastore module provides a FlowSettings implementation 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 FlowSettings instance 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 SuspendSettings instead:

    // 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()
  8. Install Multiplatform Settings

    main

    To use Multiplatform Settings, ensure mavenCentral() is in your repositories. Add the dependency to your commonMain source-set dependencies in your Gradle configuration.

    repositories {
      mavenCentral()
    }
    
    // In your build.gradle.kts
    commonMain {
      dependencies {
        implementation("com.russhwolf:multiplatform-settings:1.3.0")
      }
    }
  9. Use the Settings API to store and retrieve values

    main

    The Settings API provides methods for storing, retrieving, and querying key-value pairs. You can use explicit putXXX/getXXX methods 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.size
  10. Make Settings observable with makeObservable()

    main

    The experimental multiplatform-settings-make-observable module provides an extension function Settings.makeObservable() in common code. This converts a standard Settings instance into an ObservableSettings instance 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()