https://kotlinlang.org logo
#getting-started
Title
# getting-started
m

Mark

07/08/2020, 5:13 AM
Is there any way to access a property delegate via an extension property? For example:
Copy code
class Settings(val sharedPreferences: SharedPreferences) {
    val appVersion: Int by <http://sharedPreferences.int|sharedPreferences.int>(APP_VERSION)
}
but I would like to do:
Copy code
val Settings.appVersion: Int by <http://sharedPreferences.int|sharedPreferences.int>(APP_VERSION)
v

Viktor Petrovski

07/08/2020, 5:49 AM
You probably can do something like:
Copy code
val Settings.appVersion(sharedPreferences: SharedPreferences): Int by <http://sharedPreferences.int|sharedPreferences.int>(APP_VERSION)
a

Adam Powell

07/08/2020, 1:31 PM
It's a bit more manual than property delegation since an extension property has nowhere to store the delegate object on the object instance you're accessing the property for. You can use a custom getter though:
Copy code
val Settings.appVersion: Int get() = sharedPreferences.getInt(APP_VERSION, 0)
m

Mark

07/09/2020, 3:23 AM
I should have put
var
instead of
val
there, but anyway, so as a (somewhat ridiculous and inefficient) workaround you could have:
Copy code
private class AppVersionValueHolder(sharedPreferences: SharedPreferences) {
    var value by <http://sharedPreferences.int|sharedPreferences.int>(APP_VERSION)
}

var Settings.appVersion: Int
    get() = AppVersionValueHolder(sharedPreferences).value
    set(value) {
        AppVersionValueHolder(sharedPreferences).value = value
    }
Here’s another workaround since I know the delegate does not use either the reference or property arg:
Copy code
private val Settings.appVersionProperty
	get() = <http://sharedPreferences.int|sharedPreferences.int>(APP_VERSION)

var Settings.appVersion: Int
    get() = appVersionProperty.getValue(Any(), ???)
    set(value) {
		appVersionProperty.setValue(Any(), ???, value)
But what fake value can be given for the property? So, better for the
int()
extension function to actually return an implementation of something like:
Copy code
interface GetterSetter<T> {
    fun getValue(): T
    fun setValue(value: T)
}
and provide an extension function to convert that to a property:
Copy code
fun <T> GetterSetter<T>.asProperty() = object: ReadWriteProperty<Any, T> {
    override fun getValue(thisRef: Any, property: KProperty<*>): T = getValue()
    override fun setValue(thisRef: Any, property: KProperty<*>, value: T) = setValue(value)
}
So finally we have both ways available:
Copy code
private val Settings.appVersionGetterSetter
	get() = sharedPreferences.intGetterSetter(APP_VERSION)

var Settings.appVersion: Int
    get() = appVersionGetterSetter.getValue()
    set(value) {
		appVersionGetterSetter.setValue(value)
    }
    
class Settings(val sharedPreferences: SharedPreferences) {
	var somethingElse by sharedPreferences.intGetterSetter(SOMETHING_ELSE).asProperty()
}
Of course, this wouldn’t make any sense if the getter and setter is just doing simple
getInt
,
putInt
calls, but might be useful if there are some sanity checks and/or exception handling going on.
t

Tobias Berger

07/09/2020, 2:45 PM
I spent some thought on why the initial delegate version wasn't possible. I think i figured it out and afterwards it seems somewhat obvious. The key thing to keep in mind is that all extensions in Kotlin are basically just static methods in Java speak (and also on JVM bytecode level). The delegate doesn't change the object structure, it rather syntactic sugar for implementing functionality that only uses already available parts of an object. This is also why you can't use any private methods or properties in extensions. Also, because the extension is static and not directly linked to an instance, you can't define additional field-based properties as extension properties. Property delegates are also basically just objects with certain functions that replace the property's getter (and setter). This object needs to be stored somewhere in connection with the instance it depends on. This is impossible for extensions (again, because they are static). Also, your extension property delegate is initialized when the defining file is loaded. At that point your Settings instance is not available anywhere, so it can't be used to get a preferences delegate.
so in the case you described, I personally would prefer to just implement your delegated properties inside the Settings class and make the sharedPreferences private. If you really want to use extension properties for this, it is still possible. You just have to write a delegate class that doesn't require the Settings/sharedPreferences object for initialization but fetches it from the
thisRef
parameter in its get/setValue functions.
👍 1
m

Mark

07/10/2020, 6:59 AM
Thanks very much @Tobias Berger. Using
thisRef
makes so much more sense. My
GetterSetter
now becomes:
Copy code
interface PreferencesGetterSetter<T> {
    fun SharedPreferences.getValue(): T
    fun SharedPreferences.Editor.setValue(value: T): Boolean
}
and can be converted to a property in a similar way as before:
Copy code
fun <T> PreferencesGetterSetter<T>.asProperty() = object : ReadWriteProperty<SharedPreferences, T> {
    override fun getValue(thisRef: SharedPreferences, property: KProperty<*>): T = thisRef.getValue()
    override fun setValue(thisRef: SharedPreferences, property: KProperty<*>, value: T) =
        thisRef.edit { setValue(value) }
}
which will work for concrete properties of any class implementing
SharedPreferences
. For classes that don’t we have:
Copy code
fun <T> ReadWriteProperty<SharedPreferences, T>.of(
    sharedPreferences: SharedPreferences
): ReadWriteProperty<Any, T> {
    return object : ReadWriteProperty<Any, T> {
        override fun getValue(thisRef: Any, property: KProperty<*>): T {
            return this@of.getValue(sharedPreferences, property)
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
            this@of.setValue(sharedPreferences, property, value)
        }
    }
}
and finally for extension properties we can use:
Copy code
fun <R, T> ReadWriteProperty<SharedPreferences, T>.of(sharedPreferencesLambda: R.() -> SharedPreferences) =
    object : ReadWriteProperty<R, T> {
        override fun getValue(thisRef: R, property: KProperty<*>): T {
            return this@of.getValue(thisRef.sharedPreferencesLambda(), property)
        }

        override fun setValue(thisRef: R, property: KProperty<*>, value: T) {
            this@of.setValue(thisRef.sharedPreferencesLambda(), property, value)
        }
    }
So now we have:
Copy code
var Settings.appVersion by int(APP_VERSION).of { sharedPreferences }

class Settings(val sharedPreferences: SharedPreferences) {
	var somethingElse by int(SOMETHING_ELSE).of { sharedPreferences }
}
See here for more: https://gist.github.com/marcardar/18ca4f53deb9f451cd431e3979fb59a5
I updated the API so that the common use cases are simplified and we no longer need to expose the sharedPreferences!:
Copy code
class MySettings1(): SharedPreferences {
    var myInt1 by int("myPrefKey1")
    ...
}

var MySettings1.myInt1Ext2 by <http://PreferencesGetterSetter.int|PreferencesGetterSetter.int>("myPrefKey1Ext2").asProperty()

class MySettings2(private val sharedPreferences: SharedPreferences) {
    var myInt2 by <http://sharedPreferences.int|sharedPreferences.int>("myPrefKey2")
}

class MySettings3(val sharedPreferences: SharedPreferences) {
    var myInt3 by <http://sharedPreferences.int|sharedPreferences.int>("myPrefKey3")
}

class MyClassUsingSettings3() {
    val mySettings3: MySettings3 ...
    var myInt3Outside = <http://mySettings3.sharedPreferences.int|mySettings3.sharedPreferences.int>("myPrefKey3Outside")
}

var MySettings3.myInt3Ext by <http://PreferencesGetterSetter.int|PreferencesGetterSetter.int>("myPrefKey3Ext").asProperty { sharedPreferences }

class MySettings4(private val sharedPreferences: SharedPreferences) {
    var myInt4 by int("myPrefKey4")
    // so that other classes can declare properties using the underlying sharedPreferences
    // and extension functions can declare properties on this class using the underlying sharedPreferences
    companion object: SharedPreferencesProperties<MySettings4>( { sharedPreferences } )
}

class MyClassUsingSettings4() {
    val mySettings4 = MySettings4(...)
    var myInt4Other by MySettings4.of(mySettings4).int("myPrefKey4Other")
}

var MySettings4.myInt4Ext by <http://MySettings4.int|MySettings4.int>("myPrefKey4Ext")
https://gist.github.com/marcardar/2ea4b83b261abf58cdfc8e79b030c5fe
So that specific example becomes:
Copy code
var Settings.appVersion by <http://Settings.int|Settings.int>(APP_VERSION)

class Settings(private val sharedPreferences: SharedPreferences) {
    var somethingElse by int(SOMETHING_ELSE)
    
    companion object : SharedPreferencesProperties<Settings>({ sharedPrefs }) 
}
3 Views