Skip to main content

Kotlin Delegated Properties

Introduction

Have you ever found yourself writing the same property logic again and again in different classes? Kotlin offers an elegant solution to this common problem through a feature called delegated properties. This powerful concept allows you to extract and reuse common property behaviors without repeating code.

At its core, property delegation is a design pattern that enables you to outsource the getter and setter implementations of a property to another object. This "delegate" object handles the property's underlying behavior, while your code remains clean and focused on its primary purpose.

In this article, we'll explore Kotlin's delegated properties, understand how they work, and see how they can make your code more maintainable and concise.

Understanding Delegation in Kotlin

Before diving into delegated properties, let's briefly understand what delegation means. Delegation is a design pattern where one object handles a request by delegating operations to a second object (the delegate).

In Kotlin, the language provides first-class support for delegation through the by keyword. For properties, this means:

kotlin
class Example {
var property: String by Delegate()
}

Here, the property is delegated to an instance of Delegate(), which will handle its getter and setter operations.

How Delegated Properties Work

A delegate class must implement operator functions getValue() and (for mutable properties) setValue(). Here's the basic structure:

kotlin
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Value for ${property.name}"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("Value of ${property.name} changed to $value")
}
}

// Using the delegate
class Example {
var name: String by Delegate()
}

fun main() {
val example = Example()

// Accessing the property calls getValue() internally
println(example.name)

// Assigning to the property calls setValue() internally
example.name = "New Value"
}

Output:

Value for name
Value of name changed to New Value

When you access example.name, Kotlin calls getValue() on the delegate. Similarly, when you assign a value to example.name, Kotlin calls setValue() on the delegate.

Built-in Property Delegates

Kotlin's standard library provides several ready-to-use delegate implementations:

1. lazy()

The lazy() delegate computes the value only upon first access and caches it for subsequent accesses.

kotlin
class LazyExample {
val expensiveResource: String by lazy {
println("Computing expensive resource...")
// Simulate expensive computation
Thread.sleep(1000)
"Expensive Resource Result"
}
}

fun main() {
val example = LazyExample()
println("Object created, but resource not yet computed")

// First access computes the value
println(example.expensiveResource)

// Second access retrieves the cached value
println(example.expensiveResource)
}

Output:

Object created, but resource not yet computed
Computing expensive resource...
Expensive Resource Result
Expensive Resource Result

The expensive computation happens only once, when expensiveResource is first accessed.

2. observable()

The observable() delegate lets you react to property changes:

kotlin
import kotlin.properties.Delegates

class User {
var name: String by Delegates.observable("Initial Value") { property, oldValue, newValue ->
println("${property.name} changed from '$oldValue' to '$newValue'")
}
}

fun main() {
val user = User()
user.name = "Alice"
user.name = "Bob"
}

Output:

name changed from 'Initial Value' to 'Alice'
name changed from 'Alice' to 'Bob'

3. vetoable()

vetoable() is similar to observable() but allows you to reject changes:

kotlin
import kotlin.properties.Delegates

class PositiveNumber {
var value: Int by Delegates.vetoable(0) { _, oldValue, newValue ->
if (newValue > 0) {
true // Accept the change
} else {
println("Rejecting value $newValue - must be positive!")
false // Reject the change
}
}
}

fun main() {
val number = PositiveNumber()
number.value = 10
println("Current value: ${number.value}")

number.value = -5
println("Current value: ${number.value}")

number.value = 20
println("Current value: ${number.value}")
}

Output:

Current value: 10
Rejecting value -5 - must be positive!
Current value: 10
Current value: 20

The negative value is rejected, so the property keeps its previous value.

4. notNull()

notNull() starts as null but must be initialized before use:

kotlin
import kotlin.properties.Delegates

class Configuration {
var serverUrl: String by Delegates.notNull()

fun initialize(url: String) {
serverUrl = url
}
}

fun main() {
val config = Configuration()

try {
// Accessing before initialization
println(config.serverUrl)
} catch (e: IllegalStateException) {
println("Error: ${e.message}")
}

config.initialize("https://example.com")
println("Server URL: ${config.serverUrl}")
}

Output:

Error: Property serverUrl should be initialized before get.
Server URL: https://example.com

5. Map Delegation

Properties can be delegated to a map, which is useful for dynamic properties:

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

fun main() {
val user = User(mapOf(
"name" to "John",
"age" to 25
))

println("Name: ${user.name}")
println("Age: ${user.age}")
}

Output:

Name: John
Age: 25

Similarly, you can use a MutableMap for mutable properties:

kotlin
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}

fun main() {
val map = mutableMapOf(
"name" to "John",
"age" to 25
)

val user = MutableUser(map)
user.name = "Alice"
user.age = 30

println("User: ${user.name}, ${user.age}")
println("Map after changes: $map")
}

Output:

User: Alice, 30
Map after changes: {name=Alice, age=30}

Creating Custom Property Delegates

You can create your own property delegates for specialized behavior. Let's implement a delegate that logs all property operations:

kotlin
import kotlin.reflect.KProperty

class LoggingDelegate<T>(private var value: T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Getting value of ${property.name}: $value")
return value
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
println("Setting value of ${property.name} from $value to $newValue")
value = newValue
}
}

class Person {
var name: String by LoggingDelegate("Unknown")
var age: Int by LoggingDelegate(0)
}

fun main() {
val person = Person()

// Read properties
println("Name is: ${person.name}")
println("Age is: ${person.age}")

// Update properties
person.name = "Alice"
person.age = 30

// Read updated properties
println("Updated name: ${person.name}")
println("Updated age: ${person.age}")
}

Output:

Getting value of name: Unknown
Name is: Unknown
Getting value of age: 0
Age is: 0
Setting value of name from Unknown to Alice
Setting value of age from 0 to 30
Getting value of name: Alice
Updated name: Alice
Getting value of age: 30
Updated age: 30

Real-world Applications

Let's explore some practical applications of delegated properties.

1. Form Validation

Delegated properties can simplify form validation:

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class ValidatedString(
private var value: String = "",
private val validator: (String) -> Boolean
) : ReadWriteProperty<Any?, String> {

override fun getValue(thisRef: Any?, property: KProperty<*>): String = value

override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
if (validator(value)) {
this.value = value
} else {
throw IllegalArgumentException("Invalid value for ${property.name}: $value")
}
}
}

// Validator functions
fun isValidEmail(email: String): Boolean = email.matches(".+@.+\\..+".toRegex())
fun isValidPassword(password: String): Boolean = password.length >= 8

class UserForm {
var email: String by ValidatedString(validator = ::isValidEmail)
var password: String by ValidatedString(validator = ::isValidPassword)

fun submit(): Boolean {
println("Form submitted with email: $email")
return true
}
}

fun main() {
val form = UserForm()

try {
form.email = "invalid"
} catch (e: IllegalArgumentException) {
println(e.message)
}

form.email = "[email protected]"

try {
form.password = "123"
} catch (e: IllegalArgumentException) {
println(e.message)
}

form.password = "securePassword123"
form.submit()
}

Output:

Invalid value for email: invalid
Invalid value for password: 123
Form submitted with email: [email protected]

2. Preferences Wrapper

Delegated properties can wrap Android's SharedPreferences or similar storage:

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

// Simplified SharedPreferences mock
class SharedPreferences {
private val data = mutableMapOf<String, Any?>()

fun putString(key: String, value: String) { data[key] = value }
fun getString(key: String, default: String): String = data[key] as? String ?: default

fun putInt(key: String, value: Int) { data[key] = value }
fun getInt(key: String, default: Int): Int = data[key] as? Int ?: default

fun putBoolean(key: String, value: Boolean) { data[key] = value }
fun getBoolean(key: String, default: Boolean): Boolean = data[key] as? Boolean ?: default
}

class StringPreference(
private val prefs: SharedPreferences,
private val key: String,
private val defaultValue: String
) : ReadWriteProperty<Any?, String> {

override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return prefs.getString(key, defaultValue)
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
prefs.putString(key, value)
}
}

class AppSettings(prefs: SharedPreferences) {
var username by StringPreference(prefs, "username", "")
var theme by StringPreference(prefs, "theme", "light")
}

fun main() {
val prefs = SharedPreferences()
val settings = AppSettings(prefs)

// Default values
println("Default username: '${settings.username}'")
println("Default theme: ${settings.theme}")

// Update settings
settings.username = "john_doe"
settings.theme = "dark"

println("Updated username: ${settings.username}")
println("Updated theme: ${settings.theme}")
}

Output:

Default username: ''
Default theme: light
Updated username: john_doe
Updated theme: dark

3. Database Column Mapping

Delegated properties can simplify object-relational mapping:

kotlin
import kotlin.reflect.KProperty

// Mock database related classes
class ResultSet {
private val data = mapOf(
"id" to 1,
"name" to "John Smith",
"email" to "[email protected]",
"active" to true
)

fun getInt(column: String): Int = data[column] as Int
fun getString(column: String): String = data[column] as String
fun getBoolean(column: String): Boolean = data[column] as Boolean
}

class DBColumn<T>(private val columnName: String, private val getter: ResultSet.(String) -> T) {
operator fun getValue(thisRef: Entity, property: KProperty<*>): T {
return thisRef.resultSet.getter(columnName)
}
}

abstract class Entity(val resultSet: ResultSet)

class User(resultSet: ResultSet) : Entity(resultSet) {
val id by DBColumn("id", ResultSet::getInt)
val name by DBColumn("name", ResultSet::getString)
val email by DBColumn("email", ResultSet::getString)
val isActive by DBColumn("active", ResultSet::getBoolean)
}

fun main() {
// In a real scenario, this would come from a database query
val resultSet = ResultSet()
val user = User(resultSet)

println("User ID: ${user.id}")
println("User name: ${user.name}")
println("User email: ${user.email}")
println("User is active: ${user.isActive}")
}

Output:

User ID: 1
User name: John Smith
User email: [email protected]
User is active: true

Performance Considerations

While delegated properties provide elegant solutions for many problems, they do introduce a small overhead:

  1. Each property access creates additional objects
  2. Method calls are needed to access values
  3. For simple properties, direct implementation might be more efficient

However, for most applications, this overhead is negligible compared to the benefits of cleaner code and better maintainability.

Summary

Kotlin's delegated properties offer a powerful mechanism to:

  1. Reuse common property behavior across different classes
  2. Implement complex patterns with minimal boilerplate
  3. Keep code focused on business logic rather than property implementation details
  4. Enhance readability by separating concerns

We've explored built-in delegates like lazy(), observable(), and notNull(), as well as custom delegates for validation, preferences storage, and database mapping. These patterns can significantly improve your code's maintainability and clarity.

Next time you find yourself writing repetitive property code, consider whether a delegated property might provide a cleaner solution!

Exercises

  1. Create a PropertyHistory<T> delegate that keeps track of all values assigned to a property over time.
  2. Implement a ThreadSafe<T> delegate that ensures thread-safe access to a property using synchronization.
  3. Create a BoundedValue delegate for numeric properties that keeps values within specified minimum and maximum bounds.
  4. Implement a CachedResource delegate that loads a network resource and caches it with a configurable expiration time.
  5. Design a delegate that automatically saves property changes to a file and loads them when accessed.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)