Skip to content

Commit

Permalink
Runtime Observable Settings (#184)
Browse files Browse the repository at this point in the history
* add runtime observable settings

* fix: rebase issue

* add generated api files

* api and test improvement for RuntimeObservableSetting
  • Loading branch information
psuzn authored Mar 25, 2024
1 parent b44845e commit ab87526
Show file tree
Hide file tree
Showing 10 changed files with 437 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public final class com/russhwolf/settings/runtime_observable/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class com/russhwolf/settings/serialization/ConverterKt {
public static final fun toRuntimeObservable (Lcom/russhwolf/settings/Settings;)Lcom/russhwolf/settings/ObservableSettings;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public final class com/russhwolf/settings/serialization/ConverterKt {
public static final fun toRuntimeObservable (Lcom/russhwolf/settings/Settings;)Lcom/russhwolf/settings/ObservableSettings;
}

75 changes: 75 additions & 0 deletions multiplatform-settings-runtime-observable/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.konan.target.Family

/*
* 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 {
id("standard-configuration")
id("module-publication")
}

kotlin {

androidTarget {
publishAllLibraryVariants()
}
iosArm64()
iosSimulatorArm64()
iosX64()
js {
browser()
}
jvm()
macosArm64()
macosX64()
mingwX64()
tvosArm64()
tvosSimulatorArm64()
tvosX64()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
watchosArm32()
watchosArm64()
watchosDeviceArm64()
watchosSimulatorArm64()
watchosX64()

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

implementation(libs.kotlin.test)
}
}
}
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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] around the [Settings] instance.
*
* If the [this] is already a [ObservableSettings] it doesn't create a
* [RuntimeObservableSettingsWrapper] for it and the same instance is returned.
*
*/
public fun Settings.toRuntimeObservable(): ObservableSettings =
if (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,29 @@
import com.russhwolf.settings.MapSettings
import com.russhwolf.settings.Settings
import com.russhwolf.settings.serialization.toRuntimeObservable

/*
* 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 {
// delegating to MapSettings rather than using it directly because MapSettings is already
// observable, and delegating to it make the delegate non observable
val delegate = object : Settings by MapSettings() {}
return delegate.toRuntimeObservable()
}
}
Loading

0 comments on commit ab87526

Please sign in to comment.