Skip to content

Commit

Permalink
add runtime observable settings
Browse files Browse the repository at this point in the history
  • Loading branch information
psuzn committed Feb 25, 2024
1 parent ea3b96d commit 109671d
Show file tree
Hide file tree
Showing 8 changed files with 425 additions and 1 deletion.
1 change: 1 addition & 0 deletions multiplatform-settings-runtime-observable/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
71 changes: 71 additions & 0 deletions multiplatform-settings-runtime-observable/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2019 Russell Wolf
*
* 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.
*/

plugins {
kotlin("multiplatform")
id("com.android.library")
id("org.jetbrains.dokka")
`maven-publish`
signing
}

standardConfiguration(
"android",
"iosArm32",
"iosArm64",
"iosSimulatorArm64",
"iosX64",
"js",
"jvm",
"linuxArm32Hfp",
"linuxArm64",
"linuxX64",
"macosArm64",
"macosX64",
"mingwX64",
"mingwX86",
"tvosArm64",
"tvosSimulatorArm64",
"tvosX64",
"watchosArm32",
"watchosArm64",
"watchosSimulatorArm64",
"watchosX64",
"watchosX86"
)

kotlin {
sourceSets {
commonMain {
dependencies {
implementation(project(":multiplatform-settings"))
}
}
commonTest {
dependencies {
implementation(project(":multiplatform-settings-test"))
implementation(project(":tests"))

implementation(kotlin("test"))
}
}
}
}

android {
namespace = "com.russhwolf.settings.runtime_observable"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2024 Russell Wolf
*
* 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.
*/

package com.russhwolf.settings.serialization

import com.russhwolf.settings.ObservableSettings
import com.russhwolf.settings.Settings

/**
* Creates a [RuntimeObservableSettingsWrapper] wrapper around the [Settings] instance.
*
* @param checkObservable flag that indicates whether to skip the observable check for current
* instance or not. If it is false and the [Settings] instance is already a [ObservableSettings]
* it doesn't create a [RuntimeObservableSettingsWrapper] for it.
*
*/
public fun Settings.toRuntimeObservable(checkObservable: Boolean = true): ObservableSettings =
if (checkObservable && this is ObservableSettings) this
else RuntimeObservableSettingsWrapper(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Copyright 2024 Russell Wolf
*
* 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.
*/

package com.russhwolf.settings.serialization

import com.russhwolf.settings.ObservableSettings
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SettingsListener
import com.russhwolf.settings.get
import kotlin.jvm.JvmInline

/**
* A wrapper around provided [Settings] instance. It only ensures the callback if the
* settings are modified through the member functions of [RuntimeObservableSettingsWrapper].
*
* If the provided delegate is already
*/
internal class RuntimeObservableSettingsWrapper(
private val delegate: Settings,
) : Settings by delegate, ObservableSettings {

private val listenerMap = mutableMapOf<String, MutableSet<() -> Unit>>()

override fun remove(key: String) {
delegate.remove(key)
invokeListeners(key)
}

override fun clear() {
delegate.clear()
invokeAllListeners()
}

override fun putInt(key: String, value: Int) {
delegate.putInt(key, value)
invokeListeners(key)
}

override fun putLong(key: String, value: Long) {
delegate.putLong(key, value)
invokeListeners(key)
}

override fun putString(key: String, value: String) {
delegate.putString(key, value)
invokeListeners(key)
}

override fun putFloat(key: String, value: Float) {
delegate.putFloat(key, value)
invokeListeners(key)
}

override fun putDouble(key: String, value: Double) {
delegate.putDouble(key, value)
invokeListeners(key)
}

override fun putBoolean(key: String, value: Boolean) {
delegate.putBoolean(key, value)
invokeListeners(key)
}

override fun addIntListener(
key: String,
defaultValue: Int,
callback: (Int) -> Unit
): SettingsListener = addListener<Int>(key) {
callback(getInt(key, defaultValue))
}


override fun addLongListener(
key: String,
defaultValue: Long,
callback: (Long) -> Unit
): SettingsListener = addListener<Long>(key) {
callback(getLong(key, defaultValue))
}

override fun addStringListener(
key: String,
defaultValue: String,
callback: (String) -> Unit
): SettingsListener = addListener<String>(key) {
callback(getString(key, defaultValue))
}

override fun addFloatListener(
key: String,
defaultValue: Float,
callback: (Float) -> Unit
): SettingsListener = addListener<Float>(key) {
callback(getFloat(key, defaultValue))
}

override fun addDoubleListener(
key: String,
defaultValue: Double,
callback: (Double) -> Unit
): SettingsListener = addListener<Double>(key) {
callback(getDouble(key, defaultValue))
}

override fun addBooleanListener(
key: String,
defaultValue: Boolean,
callback: (Boolean) -> Unit
): SettingsListener = addListener<Boolean>(key) {
callback(getBoolean(key, defaultValue))
}

override fun addIntOrNullListener(
key: String, callback: (Int?) -> Unit
): SettingsListener = addListener<Int>(key) {
callback(getIntOrNull(key))
}

override fun addLongOrNullListener(
key: String,
callback: (Long?) -> Unit
): SettingsListener = addListener<Long>(key) {
callback(getLongOrNull(key))
}

override fun addStringOrNullListener(
key: String,
callback: (String?) -> Unit
): SettingsListener = addListener<String>(key) {
callback(getStringOrNull(key))
}

override fun addFloatOrNullListener(
key: String,
callback: (Float?) -> Unit
): SettingsListener = addListener<Float>(key) {
callback(getFloatOrNull(key))
}

override fun addDoubleOrNullListener(
key: String,
callback: (Double?) -> Unit
): SettingsListener = addListener<Double>(key) {
callback(getDoubleOrNull(key))
}

override fun addBooleanOrNullListener(
key: String,
callback: (Boolean?) -> Unit
): SettingsListener = addListener<Boolean>(key) {
callback(getBooleanOrNull(key))
}

private inline fun <reified T> addListener(
key: String,
noinline callback: () -> Unit
): SettingsListener {
var prev: T? = delegate[key]

val listener = {
val current: T? = delegate[key]
if (prev != current) {
callback()
prev = current
}
}

val listeners = listenerMap.getOrPut(key) { mutableSetOf() }
listeners += listener

return Listener {
removeListener(key, listener)
}
}

private fun removeListener(key: String, listener: () -> Unit) {
listenerMap[key]?.also {
it -= listener
}
}

private fun invokeListeners(key: String) {
listenerMap[key]?.forEach { callback ->
callback()
}
}

private fun invokeAllListeners() {
listenerMap.forEach { entry ->
entry.value.forEach { callback ->
callback()
}
}
}

}


/**
* A handle to a listener instance returned by one of the addListener methods of [RuntimeObservableSettingsWrapper], so it can be
* deactivated as needed.
*/
@JvmInline
internal value class Listener(
private val removeListener: () -> Unit
) : SettingsListener {

override fun deactivate(): Unit = removeListener()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import com.russhwolf.settings.BaseSettingsTest
import com.russhwolf.settings.MapSettings
import com.russhwolf.settings.Settings
import com.russhwolf.settings.get
import com.russhwolf.settings.minusAssign
import com.russhwolf.settings.serialization.RuntimeObservableSettingsWrapper
import com.russhwolf.settings.set
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

/*
* Copyright 2024 Russell Wolf
*
* 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.
*/

internal class RuntimeObservableSettingFactory : Settings.Factory {

override fun create(name: String?): Settings {
return RuntimeObservableSettingsWrapper(MapSettings())
}
}


class RuntimeObservableSettingsWrapperTest : BaseSettingsTest(
platformFactory = RuntimeObservableSettingFactory(),
allowsDuplicateInstances = false,
hasNamedInstances = false,
) {

@Test
fun delegateTest() {
val delegate = MapSettings()

delegate["key"] = "test_value"

val runtimeObservable = RuntimeObservableSettingsWrapper(delegate)

assertEquals("test_value", runtimeObservable["key"])

delegate -= "key"

assertNull(runtimeObservable.getStringOrNull("key"))
}

}
Loading

0 comments on commit 109671d

Please sign in to comment.