Kotlin: Delegated Properties and the by Keyword

Photo by Johan Mouchet on Unsplash
Photo by Johan Mouchet on Unsplash
Kotlin’s by keyword provides the function of delegated properties. Using delegated properties, you can write code shorter and more elegant.

Kotlin’s by keyword provides the function of delegated properties. Using delegated properties, you can write code shorter and more elegant. This article will introduce how to use delegated properties with the by keywords.

Standard Delegates

Kotlin has some useful standard delegates. Before we start implementing our own delegates, let’s see how to use them.

Lazy Properties

lazy is a very commonly used delegate. It allows us to defer the initialization of a property until the first use of the property.

val lazyMessage: String by lazy {
    println("Initializing")
    "Hello World!"
}

fun main() {
    println("Printing lazyMessage")
    println(lazyMessage)
}

// Output:
// Printing lazyMessage
// Initializing
// Hello World!

Observable Properties

observable delegate allows us to implement a simple version of the Observable Pattern.

var observableMessage: String by Delegates.observable("Hello World!") { property, oldValue, newValue ->
    println("Set ${property.name}, $oldValue -> $newValue")
}

fun main() {
    println("Reading observableMessage")
    observableMessage = "Hello Wayne's Talk!"
    println("Done")
}

// Output:
// Reading observableMessage
// Set observableMessage, Hello World! -> Hello Wayne's Talk!
// Done

Votable Properties

votable delegate is a variant of an observable delegate. When the property is being modified, it allows you to decide whether to allow the modification.

var votableMessage: String by Delegates.vetoable("Hello World!") { property, oldValue, newValue ->
    if (newValue.startsWith("Hello")) {
        println("Allow setting ${property.name}, $oldValue -> $newValue")
        true
    } else {
        println("Reject setting ${property.name}, $oldValue -> $newValue")
        false
    }
}

fun main() {
    votableMessage = "Wayne's Talk!"
    println(votableMessage)
    votableMessage = "Hello Wayne's Talk!"
    println(votableMessage)
}

// Output:
// Reject setting votableMessage, Hello World! -> Wayne's Talk!
// Hello World!
// Allow setting votableMessage, Hello World! -> Hello Wayne's Talk!
// Hello Wayne's Talk!

NotNull Properties

notNull delegate is similar to lateinit keyword, they allow you to declare a property without first initializing it. However, lateinit does not support primitive types (such as Int and Long), you will have to use the notNull delegate instead.

// lateinit var amount: Long // Compilation Error
var amount: Long by Delegates.notNull()

fun main() {
    try {
        println("amount is $amount")
    } catch (e: IllegalStateException) {
        println("Please set amount before read it: $e")
    }
    amount = 100
    println("amount is $amount")
}

// Output:
// Please set amount before read it: java.lang.IllegalStateException: Property amount should be initialized before get.
// amount is 100

Implementing a Delegate

We’ve looked at several standard delegates, and I’m sure you are fairly familiar with how to use them. Next, we will introduce how to implement a delegate. The implementation is very simple, we only need to declare a class and implement getValue() and setValue().

These two methods are defined in ReadOnlyProperty and ReadWriteProperty. However, we don’t need to implement these two interfaces in order to implement getValue() and setValue(). If your delegate is read-only, you only need to implement getValue().

public fun interface ReadOnlyProperty<in T, out V> {
    /**
     * Returns the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @return the property value.
     */
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
    /**
     * Returns the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @return the property value.
     */
    public override operator fun getValue(thisRef: T, property: KProperty<*>): V

    /**
     * Sets the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @param value the value to set.
     */
    public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

In the following code, we implement a delegate similar to lazy delegate.

class MyLazyDelegate<T>(private val initializer: () -> T) {
    private var value: T? = null

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("Get ${property.name}")
        return synchronized(this) {
            value ?: initializer().also { value = it }
        }
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("old value = ${this.value}, new value = $value")
        this.value = value
    }
}

fun <T> myLazyDelegate(initializer: () -> T) = MyLazyDelegate(initializer)

var myTitle: String by myLazyDelegate {
    println("Initializing")
    "Hello World!"
}

fun main() {
    myTitle = "Hello Wayne's Talk!"
    println(myTitle)
}

// Output:
// old value = null, new value = Hello Wayne's Talk!
// Get myTitle
// Hello Wayne's Talk!

Delegate Provider

In addition to directly declaring properties with a certain delegate, we can also provide a provider to dynamically create a delegate. The implementation is very simple, we need to declare a class and implement provideDelegate(). In the provideDelegate(), you can implement some logic to determine which delegate to create.

In the following example, both MyDelegate and MyLazyDelegate implement ReadWriteProperty. This is because provideDelegate() must return a ReadWriteProperty. If they don’t implement ReadWriteProperty, then the compilation will be failed.

class MyDelegate<T, V>(initializer: () -> V) : ReadWriteProperty<T, V> {
    private var value: V = initializer()

    override operator fun getValue(thisRef: T, property: KProperty<*>): V {
        println("Get ${property.name}")
        return value
    }

    override operator fun setValue(thisRef: T, property: KProperty<*>, value: V) {
        println("Set ${property.name} to $value")
        this.value = value
    }
}

class MyLazyDelegate<T, V>(private val initializer: () -> V) : ReadWriteProperty<T, V> {
    private var value: V? = null

    override operator fun getValue(thisRef: T, property: KProperty<*>): V {
        println("Get ${property.name}")
        return synchronized(this) {
            value ?: initializer().also { value = it }
        }
    }

    override operator fun setValue(thisRef: T, property: KProperty<*>, value: V) {
        println("old value = ${this.value}, new value = $value")
        this.value = value
    }
}

class MyDelegateLoader<T, V>(private val lazy: Boolean, private val initializer: () -> V) {
    operator fun provideDelegate(
        thisRef: T,
        prop: KProperty<*>,
    ): ReadWriteProperty<T, V> {
        return if (lazy) MyLazyDelegate(initializer) else MyDelegate(initializer)
    }
}

fun <T, V> myDelegate(lazy: Boolean, initializer: () -> V) = MyDelegateLoader<T, V>(lazy, initializer)

var message: String by MyDelegateLoader(true) {
    println("Initializing")
    "Wayne's Talk!"
}

fun main() {
    message = "Hello Wayne's Talk!"
    println(message)
}

// Output:
// old value = null, new value = Hello Wayne's Talk!
// Get message
// Hello Wayne's Talk!

Delegate to another Property

The by keyword also allows you to delegate a property to another property.

var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

This feature is very useful when you want to rename a property and also keep backward-compatible.

class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}

Storing Properties in a Map

The by keyword can also map properties to a map. If your program needs to parse JSON a lot, this feature will save you a lot of code.

class User(map: Map<String, Any>) {
    val name: String by map
    val age: Int by map
}

val user = User(
    mapOf(
        "name" to "John Doe",
        "age" to 25
    )
)

fun main() {
    println(user.name)
    println(user.age)
}

Conclusion

There are various delegated properties applications introduced in this article, we will sometimes use them. When it is implemented in Java, we have to write a lot of code to achieve it. When there are many such codes, it will make the code messy and difficult to read. Kotlin’s by keyword can help us save these codes and make the codes more concise and elegant.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like