Property Delegates in Kotlin
Property delegates help when you have a common kind of property, so that instead of implementing the common logic every time, with property delegates we can implement them once and reuse the logic.
get() and set() corresponding to the property will be delegated to the delegate’s getValue() and setValue() functions.
The syntax is:
val/var <property name>: <Type> by <expression>.
The expression after by is a delegate.
Property delegates don’t have to implement any interface, but they have to provide a getValue() and setValue() functions (setValue is needed only for mutable properties).
Example:
class Foo {
var prop: String by Delegate()
}
/**
* When we read from prop that delegates to an instance of Delegate, the getValue() function from Delegate is called,
* where the first parameter is the object we read prop from and the second parameter holds a description of prop itself.
*/
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
fun main() {
val foo = Foo()
println(foo.prop)
foo.prop = "test"
}
Delegating to another property
A property can delegate its getter and setter to another property. This delegation is available for top-level and class properties (i.e member and extensions), to delegate a property to another property, use the proper :: qualifier in the delegate name.
var topLevelValue: Int = 10
class ClassWithDelegate(val anotherClassValue: Int)
class CustomClass(private var memberVal: Int, private val anotherClassInstance: ClassWithDelegate) {
// delegating to a member property value
var propDelegatedToMember: Int by this::memberVal
// delegating to a top level value
var propDelegatedToTopLevel: Int by ::topLevelValue
// delegating to another class's value
val propDelegatedToAnotherClass: Int by anotherClassInstance::anotherClassValue
}
// extension example
var CustomClass.extDelegated: Int by ::topLevelValue
fun main() {
val customClass = CustomClass(2, ClassWithDelegate(4))
customClass.extDelegated = 12
println(customClass.extDelegated) // this will print 12
println(topLevelValue) // this will print 12 as well since setValue of extDelegated is delegating to topLevelValue
}
Local delegated properties
it’s possible to declare local variables (like variables defined in a function) as delegated properties.
Example:
class Bar {
fun someAction() { }
}
fun myFunction(compute: () -> Bar) {
val bar by lazy { compute() }
if (preCondition()) {
bar.someAction() // bar will be initialized only if preCondition returns true
}
}
private fun preCondition(): Boolean {
return false
}
fun main() {
myFunction {
println("called") // called will be printed only if preCondition() returned true
Bar()
}
}
Requirements for a property Delegate
For a read-only property - val - a delegate needs to just provide getValue.
For a mutable property - var - a delegate needs to provide getValue and setValue functions
these getValue and setValue functions need to match the following predefined getValue and setValue signature:
operator fun getValue(thisRef: T, property: KProperty<*>): V
operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
where T is the type of object that owns the delegated property and V is the type of the property value.
thisRef is the property owner or the type being extended, kotlin provides two interfaces to help with writing these functions ReadOnlyProperty and ReadWriteProperty.
those interfaces are only there for ease of use, the only requirement is that the delegate object implements the operator fun(s) getValue/setValue.
Example:
class Delegate2: ReadWriteProperty<Any?, String> {
override operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
class Foo2 {
var prop: String by Delegate2()
}
fun main() {
val foo = Foo2()
println(foo.prop)
foo.prop = "test"
}
let’s take a look at what does kotlin generates for a property delegate: to do that let’s consider this example:
class Player(val username: String, val score: Int)
class PlayerDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>):
Player = readPlayerData()
operator fun setValue(thisRef: Any?, property: KProperty<*>, player:Player) {
savePlayerData(player)
}
private fun readPlayerData() = Player("test", 10)
private fun savePlayerData(player: Player) { }
}
fun main() {
var player: Player by PlayerDelegate() // C1
println(player.username)
player = Player("player1",100)
}
kotlin will compile the line of code marked with C1 to something similar to:
var p$delegate = PlayerDelegate()
var player: Player
get() = p$delegate.getValue(this, ::player)
set(value) {
p$delegate.setValue(this, ::player, value)
}
we can see from the generated code that we are delegating the setter and getter calls to the delegate, we’re also passing to getValue this representing thisRef parameter which is the container where the delegate is used and ::player representing a member reference to player property that evaluates to a value of type KProperty<*>, this hold the property metadata, such as property name, type ..etc
so we have the correct parameter types passed to getValue(thisRef: T, property: KProperty<*>): V
thisRef property can be used to limit where a delegate can be used, for example we can define delegate that can be used only in a Activity class in Android:
class ActivityDelegate {
operator fun getValue(thisRef: Activity, property: KProperty<*>): Boolean = thisRef.intent
.getBooleanExtra("ExtraKey", false)
}
Now that we covered the basics, let’s learn through examples what property delegates can do:
Example 1: A delegate for setting a value to uniquely generate an id that gets initialized once and reused on subsequent calls to get:
class UniqueId : ReadOnlyProperty<Any, String> {
private var value: String? = null
override fun getValue(thisRef: Any, property: KProperty<*>): String {
val currentValue = value
return if (currentValue == null) {
val id = generateUniqueId(thisRef)
value = id
id
} else {
currentValue
}
}
private fun generateUniqueId(thisRef: Any) = thisRef::class.java.simpleName + "_" + UUID.randomUUID().toString()
}
// you can define a helper function
fun uniqueId(): ReadOnlyProperty<Any, String> = UniqueId()
class Example1 {
val id by uniqueId()
}
Example 2: A Float Range Delegate which constrains its value between a minimum and maximum.
private class FloatRangeDelegate(
val min: Float,
val max: Float): ReadWriteProperty<Any?, Float> {
private var _value: Float = 2f
override operator fun getValue(thisRef: Any?, property: KProperty<*>): Float = _value
override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Float) {
_value = value.coerceIn(min, max)
}
}
class Example2 {
var scaleFactor1 by FloatRangeDelegate(2f, 10f)
var scaleFactor2 by FloatRangeDelegate(2f, 10f)
fun updateScale(s1: Float, s2: Float) {
scaleFactor1 = s1
scaleFactor2 = s2
}
}
fun main() {
val example = Example2().also { it.updateScale(0f, 12f)}
println(example.scaleFactor1) // prints 2f since 0f gets coerced to min value 2f
println(example.scaleFactor2) // prints 10f since 12f gets coerced to max value 10f
}
Examples of Property Delegates in Android
Property delegates can help with reusing code in Android and avoid boilerplate code in places where we used to suffer from it, let’s explore some examples:
Example 1:
A lazy property that gets cleaned up when the fragment’s view is destroyed, this example demonstrates the pattern of combining a property delegate with lifecycle observers to guarantee that we do some action on a a lifecycle event like cleaning up in onDestroy.
class AutoClearedValue<T : Any>(val fragment: Fragment) : ReadWriteProperty<Fragment, T> {
private var _value: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_value = null
}
})
}
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return _value ?: throw IllegalStateException(
"should never call auto-cleared-value get when it might not be available"
)
}
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
_value = value
}
}/**
* Creates an [AutoClearedValue] associated with this fragment.
*/
fun <T : Any> Fragment.autoCleared() = AutoClearedValue<T>(this)
// Example usage:
class Fragment: Fragment {
var binding by autoCleared<CustomFragmentBinding>()
}
Example 2:
A View Delegate which invalidates using View.postInvalidateOnAnimation when the property is set.
this is an example of doing a side effect when a property is set.
class CustomAnimatingView : View(context: Context) {
var foregroundX by InvalidateDelegate(0f)
}
class InvalidateDelegate<T : Any>(var value: T) {
operator fun getValue(thisRef: View, property: KProperty<*>) = value
operator fun setValue(thisRef: View, property: KProperty<*>, value: T) {
this.value = value
thisRef.postInvalidateOnAnimation()
}
}
Example 3: Wrapping share preferences to minimize boilerplate code in multiple places that use preferences.
private inline fun <T> SharedPreferences.delegate(
key: String? = null,
defaultValue: T,
crossinline getter: SharedPreferences.(String, T) -> T,
crossinline setter: Editor.(String, T) -> Editor
): ReadWriteProperty<Any, T> =
object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T =
getter(key ?: property.name, defaultValue)!!
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) =
edit().setter(key ?: property.name, value).apply()
}
fun SharedPreferences.int(key: String? = null, defValue: Int = 0): ReadWriteProperty<Any, Int> {
return delegate(key, defValue, SharedPreferences::getInt, Editor::putInt)
}
fun SharedPreferences.long(key: String? = null, defValue: Long = 0): ReadWriteProperty<Any, Long> {
return delegate(key, defValue, SharedPreferences::getLong, Editor::putLong)
}
// ... extensions for other types
Hope this explanation and the accompanying examples help clarifying what property delegates are and how to use them.
If you prefer a video version of this explanation, I made one here, check it out!
Happy Coding!