Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A Kotlin Multiplatform library for saving simple key-value data

License

NotificationsYou must be signed in to change notification settings

russhwolf/multiplatform-settings

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Linux Build StatusMac Build StatusWindows Build Status

Maven Central

Multiplatform Settings

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

Table of contents

Usage

TheSettings interface has implementations on the Android, iOS, macOS, watchOS, tvOS, JS, WasmJS, JVM, and Windowsplatforms.

Implementation Summary

The following table shows the names of implementing classes and what platforms they're available on.

ClassBacking APIPlatforms
KeychainSettings2Apple KeychainiOS, macOS, watchOS, tvOS
NSUserDefaultsSettings1User DefaultsiOS, macOS, watchOS, tvOS
PreferencesSettings1java.util.prefs.PreferencesJVM
PropertiesSettingsjava.util.PropertiesJVM
SharedPreferencesSettings1android.content.SharedPreferencesAndroid
StorageSettingsWeb Storage (localStorage)JS, WasmJS
RegistrySettings2Windows RegistryMingwX64
DataStoreSettings3androidx.datastore.core.DataStoreAndroid, JVM, Native
MapSettings1,4kotlin.collections.MutableMapAll platforms
1 ImplementsObservableSettings interface
2 Implementation is considered experimental
3 ImplementsSuspendSettings andFlowSettings rather thanSettings orObservableSettings
4MapSettings is intended for use in unit tests and will not persist data to storage

Creating a Settings instance

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

Platform constructors

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)

Factories

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.

No-arg module

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.

Settings API

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().

Listeners

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.

Testing

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.

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

Experimental API

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.

Experimental Implementations

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.

Serialization module

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.

Coroutine APIs

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()

DataStore

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()

Make-Observable module

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.

Adding to your project

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.

Building

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

License

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.

Jetbrains Logo

Made with JetBrains tools


[8]ページ先頭

©2009-2025 Movatter.jp