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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
- Each property access creates additional objects
- Method calls are needed to access values
- 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:
- Reuse common property behavior across different classes
- Implement complex patterns with minimal boilerplate
- Keep code focused on business logic rather than property implementation details
- 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
- Create a
PropertyHistory<T>
delegate that keeps track of all values assigned to a property over time. - Implement a
ThreadSafe<T>
delegate that ensures thread-safe access to a property using synchronization. - Create a
BoundedValue
delegate for numeric properties that keeps values within specified minimum and maximum bounds. - Implement a
CachedResource
delegate that loads a network resource and caches it with a configurable expiration time. - 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! :)