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.
Table of Contents
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
- Delegated properties, Kotlin Docs.