- Notifications
You must be signed in to change notification settings - Fork75
A Kotlin Multiplatform library for saving simple key-value data
License
russhwolf/multiplatform-settings
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
This is a Kotlin library for Multiplatform apps, so that common code can persist key-value data.
AKorean translationof this readme is available separately, maintained by @wooram-yang
TheSettings interface has implementations on the Android, iOS, macOS, watchOS, tvOS, JS, WasmJS, JVM, and Windowsplatforms.
The following table shows the names of implementing classes and what platforms they're available on.
| Class | Backing API | Platforms |
|---|---|---|
KeychainSettings2 | Apple Keychain | iOS, macOS, watchOS, tvOS |
NSUserDefaultsSettings1 | User Defaults | iOS, macOS, watchOS, tvOS |
PreferencesSettings1 | java.util.prefs.Preferences | JVM |
PropertiesSettings | java.util.Properties | JVM |
SharedPreferencesSettings1 | android.content.SharedPreferences | Android |
StorageSettings | Web Storage (localStorage) | JS, WasmJS |
RegistrySettings2 | Windows Registry | MingwX64 |
DataStoreSettings3 | androidx.datastore.core.DataStore | Android, JVM, Native |
MapSettings1,4 | kotlin.collections.MutableMap | All platforms |
ObservableSettings interface2 Implementation is considered experimental
3 Implements
SuspendSettings andFlowSettings rather thanSettings orObservableSettings4
MapSettings is intended for use in unit tests and will not persist data to storageWhen writing multiplatform code, you might need to interoperate with platform-specific code which needs to share thesame data-source. To facilitate this, allSettings implementations wrap a delegate object which you could also use inyour platform code.
Since that delegate is a constructor argument, it should be possible to connect it via any dependency-injection strategyyou might already be using. If your project doesn't have such a system already in place, one strategy is to useexpectdeclarations, for example
expectval settings:Settings// orexpectfuncreateSettings():Settings
Then theactual implementations can pass the platform-specific delegates.SeePlatform constructors below for more details on these delegates.
Some platform implementations also includeFactory classes. These make it easier to manage multiple namedSettingsobjects from common code, or to automate some platform-specific configuration so that delegates don't need to be createdmanually. The factory still needs to be injected from platform code, but then from common you can call
val settings1:Settings= factory.create("my_first_settings")val settings2:Settings= factory.create("my_other_settings")
SeeFactories below for more details.
However, if all of your key-value logic exists in a single instance in common code, these ways ofinstantiationSettings can be inconvenient. To make pure-common usage easier, Multiplatform Settings now includes aseparate module which provides aSettings() factory function, so that you can create aSettings instance like
val settings:Settings=Settings()
SeeNo-arg module below for more details.
The Android implementation isSharedPreferencesSettings, which wrapsSharedPreferences.
val delegate:SharedPreferences// ...val settings:Settings=SharedPreferencesSettings(delegate)
On iOS, macOS, tvOS, or watchOS,NSUserDefaultsSettings wrapsNSUserDefaults.
val delegate:NSUserDefaults// ...val settings:Settings=NSUserDefaultsSettings(delegate)
You can also useKeychainSettings which writes to the Keychain. Construct it by passing a String which will beinterpreted as a service name.
val serviceName:String// ...val settings:Settings=KeychainSettings(serviceName)
Two JVM implementations exist.PreferencesSettings wrapsPreferences andPropertiesSettings wrapsProperties.
val delegate:Preferences// ...val settings:Settings=PreferencesSettings(delegate)val delegate:Properties// ...val settings:Settings=PropertiesSettings(delegate)
On JS and WasmJS,StorageSettings wrapsStorage.
val delegate:Storage// ...val settings:Settings=StorageSettings(delegate)val settings:Settings=StorageSettings()// use localStorage by default
There is a Windows implementationRegistrySettings which wraps the Windows registry.
val rootKey:String="SOFTWARE\\..."// Will be interpreted as subkey of HKEY_CURRENT_USERval settings:Settings=RegistrySettings(rootKey)
For some platforms, aFactory class also exists, so that multiple namedSettings instances can coexist with thenames being controlled from common code.
On Android, this factory needs aContext parameter
val context:Context// ...val factory:Settings.Factory=SharedPreferencesSettings.Factory(context)
On most other platforms, the factory can be instantiated without passing any parameter
val factory:Settings.Factory=NSUserDefaultsSettings.Factory()
If you have aFactory reference from your common code, then you can use it to create multipleSettings withdifferent names.
val settings1:Settings= factory.create("my_first_settings")val settings2:Settings= factory.create("my_other_settings")
If the defaultFactorys don't do what you need, you can also implement your own.
To create aSettings instance from common without needing to pass platform-specific dependencies, addthemultiplatform-settings-no-arg gradle dependency. This exportsmultiplatform-settings as an API dependency, soyou can use it as a replacement for that default dependency.
implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0")Then from common code, you can write
val settings:Settings=Settings()
This is implemented via a top-level functionSettings() to provide constructor-likesyntax even thoughSettings has no constructor.
On Android, this delegates to the equivalent ofPreferenceManager.getDefaultSharedPreferences() internally. It makesuse ofandroidx-startup to get aContextreference without needing to pass one manually. On Apple platforms, it usesNSUserDefaults.standardUserDefaults. OnJS, it useslocalStorage. On JVM, it uses thePreferences implementation withPreferences.userRoot() as adelegate. On Windows, it reads the name of the executable being built and writes to a subkey ofHKEY_CURRENT_USER\SOFTWARE using that name.
Note that while the mainmultiplatform-settings module publishes common code to all available Kotlin platforms,themultiplatform-settings-no-arg module only publishes to platforms which have concrete implementations.
Note also that theno-arg module is there to make getting started easier with less configuration, but there are plentyof things it doesn't provide, such as the ability to use an encrypted implementation on platforms that support it, orthe ability to substitute a test implementation. Notably, you can't callSettings() from an Android unit test becausethe internals that allow it to get aContext reference won't run (not even if you use Robolectric).
If you need a non-default setup you likely are better off not usingmultiplatform-settings-no-arg.
Once theSettings instance is created, you can store values by calling the variousputXXX() methods, or theiroperator shortcuts
settings.putInt("key",3)settings["key"]=3
You can retrieve stored values via thegetXXX() methods or their operator shortcuts. If a key is not present, then thesupplied default will be returned instead.
val a:Int= settings.getInt("key")val b:Int= settings.getInt("key", defaultValue=-1)val c:Int= settings["key",-1]
Nullable methods are also available to avoid the need to use a default value. Instead,null will be returned if a keyis not present.
val a:Int?= settings.getIntOrNull("key")val b:Int?= settings["key"]
ThegetXXX() andputXXX() operation for a given key can be wrapped using a property delegate. This has the advantageof ensuring that the key is always accessed with a consistent type.
val a:Int by settings.int("key")val b:Int by settings.int("key", defaultValue=-1)
Nullable delegates exists so that absence of a key can be indicated bynull instead of a default value
val a:Int? by settings.nullableInt("key")
Thekey parameter can be omitted for delegates, and the property name will be reflectively used instead.
val a:Int by settings.int()// internally, key is "a"
Existence of a key can be queried
val a:Boolean= settings.hasKey("key")val b:Boolean="key"in settings
Values can also be removed by key
settings.remove("key")settings-="key"settings["key"]=null
Finally, all values in aSettings instance can be removed
settings.clear()
The set of keys and amount of entries can be retrieved
val keys:Set<String>= settings.keysval size:Int= settings.size
Note that for theNSUserDefaultsSettings implementation, some entries are unremovable and therefore may still bepresent after aclear() call. Thus,size is not generally guaranteed to be zero after aclear().
Update listeners are available for some implementations. These are markedwith theObservableSettings interface, which includes anaddListener() method.
val observableSettings:ObservableSettings// ...val settingsListener:SettingsListener= observableSettings.addIntListener(key) { value:Int->/* ...*/ }val settingsListener:SettingsListener= observableSettings.addNullableIntListener(key) { value:Int?->/* ...*/ }
TheSettingsListener returned from the call should be used to signal when you're done listening:
settingsListener.deactivate()
If you don't hold a strong reference to theSettingsListener, it's possible in some implementations that it will begarbage-collected and stop sending updates.
A testing dependency is available to aid in testing code that interacts with this library.
implementation("com.russhwolf:multiplatform-settings-test:1.3.0")This includes aMapSettings implementation of theSettings interface, which is backed by an in-memoryMutableMapon all platforms.
TheSettings interface is published to all available platforms. Developers who desire implementations outside of thedefaults provided are free to add their own implementations, and are welcome to make pull requests if the implementationmight be generally useful to others. Note that implementations which require external dependencies should be places in aseparate gradle module in order to keep the coremultiplatform-settings module dependency-free.
Certain APIs are marked with@ExperimentalSettingsApi or@ExperimentalSettingsImplementation to highlight areas thatmay have the potential to break in the future and should not be considered stable to depend on.
TheKeychainSettings implementation on Apple platforms and theRegistrySettings implementation on Windows areconsidered experimental. Feel free to reach out if they're working well for you, or if you encounter any issues withthem, to help remove that experimental status.
Akotlinx-serialization integration exists so it's easier to save non-primitive data
implementation("com.russhwolf:multiplatform-settings-serialization:1.3.0")This essentially uses theSettings store as a serialization format. Thus for a serializable class
@SerializableclassSomeClass(valsomeProperty:String,anotherProperty:Int)
an instance can be stored or retrieved
val someClass:SomeClassval settings:Settings// Store values for the properties of someClass in settingssettings.encodeValue(SomeClass.serializer(),"key", someClass)// Create a new instance of SomeClass based on the data in settingsval newInstance:SomeClass= settings.decodeValue(SomeClass.serializer(),"key", defaultValue)val nullableNewInstance:SomeClass= settings.decodeValueOrNull(SomeClass.serializer(),"key")
To remove a serialized value, useremoveValue() rather thanremove()
settings.removeValue(SomeClass.serializer(),"key")// Don't remove if not all expected data is presetsettings.removeValue(SomeClass.serializer(),"key", ignorePartial=true)
To check for the existance of a serialized value, usecontainsValue() rather thancontains().
val isPresent= settings.containsValue(SomeClass.serializer(),"key")
There's also a delegate API, similar to that for primitives
val someClass:SomeClass by settings.serializedValue(SomeClass.serializer(),"someClass", defaultValue)val nullableSomeClass:SomeClass? by settings.nullableSerializedValue(SomeClass.serializer(),"someClass")
All APIs also have variants that infer a serializer implicitly rather than taking one as a parameter. These APIs throwif the class is not serializable.
settings.encodeValue("key", someClass)val newInstance:SomeClass= settings.decodeValue("key", defaultValue)val nullableNewInstance:SomeClass= settings.decodeValueOrNull("key")// etc
Usage requires accepting both the@ExperimentalSettingsApi and@ExperimentalSerializationApi annotations.
A separatemultiplatform-settings-coroutines dependency includes various coroutine APIs.
implementation("com.russhwolf:multiplatform-settings-coroutines:1.3.0")This adds flow extensions for all types which use the listener APIs internally.
val observableSettings:ObservableSettings// Only works with ObservableSettingsval flow:Flow<Int> by observableSettings.getIntFlow("key", defaultValue)val nullableFlow:Flow<Int?> by observableSettings.getIntOrNullFlow("key")
There are alsoStateFlow extensions, which require a coroutine scope.
val observableSettings:ObservableSettings// Only works with ObservableSettingsval coroutineScope:CoroutineScopeval stateFlow:StateFlow<Int> by observableSettings.getIntStateFlow("key", defaultValue)val nullableStateFlow:StateFlow<Int?> by observableSettings.getIntOrNullStateFlow("key")
In addition, there are two newSettings-like interfaces:SuspendSettings, which looks similar toSettings but allfunctions are markedsuspend, andFlowSettings which extendsSuspendSettings to also includeFlow-based getterssimilar to the extensions mentioned above.
val suspendSettings:SuspendSettings// ...val a:Int= suspendSettings.getInt("key")// This call will suspendval flowSettings:FlowSettings// ...val flow:Flow<Int>= flowSettings.getIntFlow("key")
There are APIs provided to convert between these different interfaces so that you can select one to use primarily fromcommon.
val settings:Settings// ...val suspendSettings:SuspendSettings= settings.toSuspendSettings()val observableSettings:ObservableSettings// ...val flowSettings:FlowSettings= observableSettings.toFlowSettings()// Wrap suspend calls in runBlockingval blockingSettings:Settings= suspendSettings.toBlockingSettings()val blockingSettings:ObservableSettings= flowSettings.toBlockingObservableSettings()
An implementation ofFlowSettings exists in themultiplatform-settings-datastore dependency, basedonJetpack DataStore. Because DataStore is now amultiplatform library, starting in version 1.2.0, this module is available on all platforms where DataStore isavailable, rather than being limited to Android and JVM.
implementation("com.russhwolf:multiplatform-settings-datastore:1.3.0")This provides aDataStoreSettings class
val dataStore:DataStore// = ...val settings:FlowSettings=DataStoreSettings(dataStore)
You can use this in shared code by converting otherObservableSettings instances toFlowSettings. For example:
// Commonexpectval settings:FlowSettings// Androidactualval settings:FlowSettings=DataStoreSettings(/*...*/)// iOSactualval settings:FlowSettings=NSUserDefaultsSettings(/*...*/).toFlowSettings()
Or, if you also include platforms without listener support, you can useSuspendSettings instead.
// Commonexpectval settings:SuspendSettings// Androidactualval settings:SuspendSettings=DataStoreSettings(/*...*/)// iOSactualval settings:SuspendSettings=NSUserDefaultsSettings(/*...*/).toSuspendSettings()// JSactualval settings:SuspendSettings=StorageSettings().toSuspendSettings()
The experimentalmultiplatform-settings-make-observable module adds an extension functionSettings.makeObservable()in common code which converts aSettings instance toObservableSettings by directly wiring in callbacks rather thannative observability methods.
val settings:Settings// = ...val observableSettings:ObservableSettings= settings.makeObservable()
This has the advantage of enabling observability on platforms which don't have an observable implementation. It has thedisadvantage that updates will only be delivered to the same instance where changes were made.
Multiplatform Settings is currently published to Maven Central, so add that to repositories.
repositories { mavenCentral()// ...}Then, simply add the dependency to your common source-set dependencies
commonMain { dependencies {// ... implementation("com.russhwolf:multiplatform-settings:1.3.0") }}See also the sample project, which uses this structure.
The project includes multiple CI jobs configured using Github Actions. On PRs or updates to themain branch, the buildwill run the scripts inbuild-linux.yml,build-macos.yml,build-windows.yml, andvalidate-gradle-wrapper.yml.These builds the library and runs unit tests for all platforms across Linux, Mac, and Windows hosts. In addition, thelibrary build artifacts are deployed to the local maven repository and the sample project is built for the platforms onwhich it is implemented. This ensures that the sample remains in sync with updates to the library.
An addition build script is defined indeploy.yml, which runs on a manual trigger. This builds the library for allplatforms and uploads artifacts to staging on Maven Central. Uploaded artifacts must still be published manually
Copyright 2018-2023 Russell WolfLicensed 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.0Unless required by applicable law or agreed to in writing, softwaredistributed 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 andlimitations under the License.Made with JetBrains tools
About
A Kotlin Multiplatform library for saving simple key-value data
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.
