Hello!
My name is Artem and I am the author of the Android Insights YouTube channel
Today we will enter the world of delegates and delegated properties in Kotlin. This topic might seem complex at first glance, but I'll do my best to explain it as clearly as possible. Let's get started!
Before going into the details, let's clarify some fundamental concepts.
A delegate is an object that another object, well, delegates certain tasks to. In Kotlin, delegation is a powerful tool that allows you to reuse code and implement complex behavior without the need for inheritance.
Let's consider a simple example:
interface Base {
fun print()
}
class BaseImpl(private val x: Int) : Base {
override fun print() {
println(x)
}
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print() // Outputs: 10
}
In this example:
Base
with a method print()
.BaseImpl
implements this interface and holds a value x
.Derived
delegates the implementation of Base
to the object b
.When we call print()
on an instance of Derived
, it actually executes the print()
method of the BaseImpl
object. This allows us to reuse code and add flexibility to the application's architecture without excessive inheritance.
Now, let's discuss delegated properties.
A delegated property is a property that delegates its getter and setter to another object. The syntax for declaring a delegated property looks like this:
class Example {
var p: String by Delegate()
}
Here, p
is a delegated property. All accesses to p
will be redirected to the Delegate()
object.
Kotlin provides several built-in delegates that simplify common tasks.
lazy
— Lazy InitializationThe lazy
delegate is used for lazy initialization of a property. The value is computed only upon the first access.
val lazyValue: String by lazy {
println("Computing the value...")
"Hello"
}
fun main() {
println(lazyValue) // Outputs: Computing the value... Hello
println(lazyValue) // Outputs: Hello
}
In this example, when we first access lazyValue
, the code block inside lazy
is executed, and the value is stored for subsequent uses.
observable
— Observable PropertyThe observable
delegate allows you to track changes to a property and react accordingly.
import kotlin.properties.Delegates
var name: String by Delegates.observable("Initial value") { prop, old, new ->
println("$old -> $new")
}
fun main() {
name = "First" // Outputs: Initial value -> First
name = "Second" // Outputs: First -> Second
}
Here, every time name
changes, the code block is executed, printing the old and new values.
In addition to built-in delegates, we can create custom delegates to implement specific behavior. This can be done using the ReadOnlyProperty
and ReadWriteProperty
interfaces, or by directly implementing the getValue
and setValue
operators.
ReadOnlyProperty
Let's create a delegate that always returns the same value and logs each access.
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class ConstantValue<T>(private val value: T) : ReadOnlyProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Getting value of property '${property.name}'")
return value
}
}
class Example {
val constant: String by ConstantValue("Hello, World!")
}
fun main() {
val example = Example()
println(example.constant)
// Outputs:
// Getting value of property 'constant'
// Hello, World!
}
ReadWriteProperty
Now let's create a delegate that logs read and write operations of a property.
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LoggingProperty<T>(private var value: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Getting value of property '${property.name}': $value")
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
println("Setting value of property '${property.name}' to $newValue")
value = newValue
}
}
class Example {
var logged: String by LoggingProperty("Initial")
}
fun main() {
val example = Example()
println(example.logged)
example.logged = "New Value"
println(example.logged)
// Outputs:
// Getting value of property 'logged': Initial
// Initial
// Setting value of property 'logged' to New Value
// Getting value of property 'logged': New Value
// New Value
}
Using ReadOnlyProperty
and ReadWriteProperty
allows us to explicitly indicate which operations the delegate supports, making the code more readable and understandable.
getValue
and setValue
Alternatively, we can directly implement the getValue
and setValue
operators.
import kotlin.reflect.KProperty
class StringDelegate {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("Getting value of property '${property.name}'")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
println("Setting value of property '${property.name}' to $newValue")
value = newValue
}
}
class Example {
var str: String by StringDelegate()
}
fun main() {
val example = Example()
example.str = "Hello"
println(example.str)
// Outputs:
// Setting value of property 'str' to Hello
// Getting value of property 'str'
// Hello
}
This approach offers more flexibility, allowing us to implement only the necessary methods for our purposes.
Let's explore some practical scenarios where delegates can be particularly useful.
The lazy
delegate is perfect for deferred initialization of objects whose creation requires significant resources.
class ResourceManager {
val database by lazy {
println("Connecting to the database...")
Database.connect()
}
}
fun main() {
val manager = ResourceManager()
println("ResourceManager created")
// The database is not initialized yet
manager.database.query("SELECT * FROM users")
// The database is already initialized
manager.database.query("SELECT * FROM products")
}
In this example, the connection to the database occurs only upon the first access to database
, conserving resources until they are actually needed.
With the observable
delegate, we can easily implement the Observer pattern by tracking property changes.
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("") { prop, old, new ->
println("User's name changed from '$old' to '$new'")
}
}
fun main() {
val user = User()
user.name = "Alice" // Outputs: User's name changed from '' to 'Alice'
user.name = "Bob" // Outputs: User's name changed from 'Alice' to 'Bob'
}
This allows us to perform specific actions when properties change, such as updating the UI, validating data, or sending notifications.
Delegates are a powerful tool in the Kotlin developer's arsenal. They enable you to write cleaner, more modular, and flexible code, opening up new possibilities for implementing complex logic and design patterns.
I hope this guide has helped you better understand delegates and delegated properties in Kotlin. Feel free to experiment and apply these concepts in your own projects!
Thank you for your attention, and happy coding!