How can I make custom classes/functions trigger re...
# compose-desktop
m
How can I make custom classes/functions trigger recompositions? I have a custom multiplatform project to store/load settings like
val theme by settings.bound<AppTheme>(default=MaterialAppTheme(isSystemInDarkTheme()))
but if I change this value elsewhere in the code, I need to tell this part to reload and trigger a recomposition. How would I do this? I tried reading the source for MutableState but didn't quite understand how that triggered recompositions. Relevant Delegate class in thread.
Copy code
@Suppress("UNCHECKED_CAST")
class BoundSetting<T>(private val settings: Settings, private val key: String?, private val default: T?, private val type: KType, private val serializer: KSerializer<T>) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        @Suppress("IMPLICIT_CAST_TO_ANY")
        val res = when (type) {
            String::class -> settings.getStringOrNull(key ?: property.name)
            Int::class -> settings.getIntOrNull(key ?: property.name)
            Long::class -> settings.getLongOrNull(key ?: property.name)
            Float::class -> settings.getFloatOrNull(key ?: property.name)
            Double::class -> settings.getDoubleOrNull(key ?: property.name)
            Boolean::class -> settings.getBooleanOrNull(key ?: property.name)
            else -> settings.getStringOrNull(key ?: property.name)?.let { Json.decodeFromString(serializer, it) }
        }

        return (res ?: default) as T
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
        if (value == null) {
            settings.remove(key ?: property.name)
            return
        }

        when (type) {
            String::class -> settings.putString(key ?: property.name, value as String)
            Int::class -> settings.putInt(key ?: property.name, value as Int)
            Long::class -> settings
            Float::class -> settings
            Double::class -> settings
            Boolean::class -> settings
            else -> settings.putString(key ?: property.name, Json.encodeToString(serializer, value))
        }
    }

    companion object {
        inline operator fun <reified T> invoke(settings: Settings, key: String?, default: T): BoundSetting<T> {
            return BoundSetting(settings, key, default, typeOf<T>(), serializer())
        }

        inline operator fun <reified T> invoke(settings: Settings, key: String?): BoundSetting<T?> {
            return BoundSetting(settings, key, null, typeOf<T>(), serializer())
        }
    }
}
Settings.bound(...)
are just simple functions that call
BoundSetting(this, ...)
In #compose someone said they didn't understand my code but wanted me to use ViewModel (which was android-only), but ideally I don't have to overhaul my entire code and all
setValue
calls just trigger a recomposition for the relevant
getValue
calls made elsewhere. Note that I want
val x by settings.bound(...)
to be recomposed by a modification from elsewhere; Basically, if getValue would return a different value if called again, it should recompose.
Nothing outside the app will ever modify the result of
Settings.getX
calls, only
Settings.put
calls would modify it.
l
It seems like you want settings.bound to be reactive. Can you make it return a StateFlow? Then you just emit to the flow when the value changes (in setValue), and call collecAsState on the StateFlow to make Compose respond to changes.
m
But if bound() returns a StateFlow, emitting/collecting would need to be on the same object, and wouldn't support custom getters/setters as the BoundSetting class would be unused.