Delegated properties are a powerful feature in Kotlin that allow you to delegate the implementation of property accessors (getters and setters) to another object. This can help reduce boilerplate code, make your code more readable, and promote reusability. In this section, we'll explore how delegated properties work, their use cases, and best practices for using them in your Kotlin applications.
A delegated property is a property whose getter and/or setter are implemented by another object. This other object is called the delegate. When you access or modify a delegated property, the corresponding method of the delegate is invoked.
The syntax for declaring a delegated property in Kotlin is as follows:
var <property-name>: <type> by <delegate>
<property-name>: The name of the property.<type>: The type of the property.by: The keyword that indicates delegation.<delegate>: The object that will handle the property access.Let's start with a simple example to illustrate how delegated properties work:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("John") { _, oldValue, newValue ->
println("Name changed from $oldValue to $newValue")
}
}
fun main() {
val user = User()
user.name = "Jane" // Output: Name changed from John to Jane
}
In this example, the name property is delegated to an observable delegate provided by Kotlin's standard library. The lambda expression passed to observable is called whenever the value of name changes.
Kotlin provides several built-in delegates that you can use out of the box:
Delegates.observableThe observable delegate allows you to observe changes to a property and execute custom logic when the property's value changes.
import kotlin.properties.Delegates
class Counter {
var count: Int by Delegates.observable(0) { _, oldValue, newValue ->
println("Count changed from $oldValue to $newValue")
}
}
fun main() {
val counter = Counter()
counter.count = 1 // Output: Count changed from 0 to 1
counter.count = 2 // Output: Count changed from 1 to 2
}
Delegates.vetoableThe vetoable delegate allows you to veto changes to a property by returning true or false in the lambda expression.
import kotlin.properties.Delegates
class User {
var age: Int by Delegates.vetoable(18) { _, oldValue, newValue ->
if (newValue < 0) {
println("Age cannot be negative")
false
} else {
true
}
}
}
fun main() {
val user = User()
user.age = -5 // Output: Age cannot be negative
println(user.age) // Output: 18
}
Delegates.notNullThe notNull delegate ensures that a property is initialized before it's accessed.
import kotlin.properties.Delegates
class User {
var name: String by Delegates.notNull()
}
fun main() {
val user = User()
// user.name = "John" // Uncomment this line to initialize the property
println(user.name) // Throws IllegalStateException if not initialized
}
Delegates.lazyThe lazy delegate delays the initialization of a property until it's accessed for the first time.
import kotlin.properties.Delegates
class User {
val name: String by Delegates.lazy {
println("Initializing name")
"John"
}
}
fun main() {
val user = User()
println(user.name) // Output: Initializing name, then John
println(user.name) // Output: John (no reinitialization)
}
You can also create your own custom delegates by implementing the ReadWriteProperty interface. This allows you to define how a property should be accessed and modified.
Let's create a custom delegate that logs every access to a property:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LoggingDelegate<T>(private var value: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Accessing ${property.name}: $value")
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
println("Setting ${property.name} to $value")
this.value = value
}
}
class User {
var name: String by LoggingDelegate("John")
}
fun main() {
val user = User()
println(user.name) // Output: Accessing name: John, then John
user.name = "Jane" // Output: Setting name to Jane
println(user.name) // Output: Accessing name: Jane, then Jane
}
In this example, the LoggingDelegate class implements the ReadWriteProperty interface and logs every access and modification of the property.
Delegated properties are particularly useful for lazy initialization, where a resource is expensive to create or initialize.
import kotlin.properties.Delegates
class DatabaseConnection {
val connection: Connection by Delegates.lazy {
println("Connecting to database")
// Create and return the database connection
}
}
fun main() {
val db = DatabaseConnection()
println(db.connection) // Output: Connecting to database, then [connection object]
}
You can use delegated properties to enforce validation rules on property values.
import kotlin.properties.Delegates
class User {
var age: Int by Delegates.vetoable(18) { _, oldValue, newValue ->
if (newValue < 0 || newValue > 120) {
println("Invalid age")
false
} else {
true
}
}
}
fun main() {
val user = User()
user.age = -5 // Output: Invalid age
println(user.age) // Output: 18
}
Delegated properties can be used to create observable properties that notify listeners of changes.
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("John") { _, oldValue, newValue ->
println("Name changed from $oldValue to $newValue")
}
}
fun main() {
val user = User()
user.name = "Jane" // Output: Name changed from John to Jane
}
Use Built-in Delegates When Possible: Kotlin provides several built-in delegates that cover common use cases. Using these can make your code more concise and easier to understand.
Keep Delegates Simple: Custom delegates should be simple and focused on a single responsibility. Avoid overcomplicating the logic within a delegate.
Document Delegates Clearly: If you create custom delegates, ensure that their behavior is well-documented. This will help other developers understand how they work and when to use them.
Avoid Overuse: While delegated properties can be powerful, they should not be overused. Only use them when they provide a clear benefit in terms of code readability or reusability.
Test Delegates Thoroughly: Since delegates encapsulate the behavior of property accessors, it's important to test them thoroughly to ensure that they work as expected.
Delegated properties are a powerful feature in Kotlin that can help you write cleaner and more maintainable code. By delegating the implementation of property accessors to another object, you can reduce boilerplate code, promote reusability, and make your code more readable. Whether you're using built-in delegates or creating custom ones, understanding how delegated properties work will enable you to write more efficient and effective Kotlin applications.