Table of Contents
- What Are Delegated Properties?
- The Delegation Pattern in Kotlin
- Standard Delegates Provided by Kotlin
- Creating Custom Delegates
- Advanced Use Cases
- Best Practices
- Conclusion
- References
What Are Delegated Properties?
In Kotlin, a delegated property is a property whose getter and setter logic is delegated to a separate object (the “delegate”). Instead of implementing the property’s access logic directly in the class, you outsource it to a delegate, which handles getting and setting values.
This pattern promotes:
- Code reuse: Share property logic across multiple classes.
- Cleaner code: Separate property logic from the class’s core responsibility.
- Reduced boilerplate: Avoid repetitive getter/setter implementations.
The syntax for a delegated property is:
val/var <propertyName>: <Type> by <delegate>
Here, by specifies the delegate object. The delegate must implement specific methods to handle property access:
- For read-only properties (
val): AgetValuemethod. - For mutable properties (
var): BothgetValueandsetValuemethods.
The Delegation Pattern in Kotlin
Delegated properties rely on the delegation pattern, a design pattern where an object (the delegator) forwards responsibilities to another object (the delegate). In Kotlin, this is formalized with the by keyword, which automatically generates code to delegate property access to the delegate.
For example, instead of writing:
class User {
private var _name: String? = null
var name: String
get() {
println("Getting name")
return _name ?: "Default"
}
set(value) {
println("Setting name to $value")
_name = value
}
}
You can delegate the logging logic to a LoggingDelegate, simplifying User:
class User {
var name: String by LoggingDelegate("Default")
}
The LoggingDelegate encapsulates the getter/setter logic, making User focused on its core role.
Standard Delegates Provided by Kotlin
Kotlin’s standard library (kotlin.properties) includes several built-in delegates for common use cases. These are ready to use and cover most everyday needs.
Lazy Initialization (lazy)
The lazy delegate defers property initialization until the first access, making it ideal for expensive operations (e.g., network calls, database setup) that shouldn’t run until needed.
Key Features:
- Initializes the value only once (thread-safe by default).
- Returns the cached value on subsequent accesses.
Syntax:
val <propertyName>: <Type> by lazy(<mode>) { initializationBlock }
Modes:
LazyThreadSafetyMode.SYNCHRONIZED(default): Uses a lock to ensure thread safety.LazyThreadSafetyMode.NONE: No synchronization (for single-threaded environments).LazyThreadSafetyMode.PUBLICATION: Allows concurrent initialization but returns the first completed value.
Example:
val database: Database by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
println("Initializing database...")
Database.connect() // Expensive initialization
}
fun main() {
println("Before accessing database")
val db = database // First access: initializes
val db2 = database // Subsequent access: uses cached value
// Output:
// Before accessing database
// Initializing database...
}
Observable Properties (observable and vetoable)
These delegates track changes to a property, useful for validation, logging, or triggering side effects.
observable: Notifies when the property is updated.
Syntax:
var <propertyName>: <Type> by Delegates.observable(initialValue) { property, oldValue, newValue ->
// Reaction to change (e.g., log, update UI)
}
Example:
import kotlin.properties.Delegates
class User {
var age: Int by Delegates.observable(0) { prop, old, new ->
println("${prop.name} changed from $old to $new")
}
}
fun main() {
val user = User()
user.age = 25 // Output: age changed from 0 to 25
user.age = 30 // Output: age changed from 25 to 30
}
vetoable: Blocks updates unless a condition is met (acts as a “gatekeeper”).
Syntax:
var <propertyName>: <Type> by Delegates.vetoable(initialValue) { property, oldValue, newValue ->
// Return true to allow the change, false to veto
}
Example (prevent negative ages):
class User {
var age: Int by Delegates.vetoable(0) { _, old, new ->
new >= 0 // Allow only non-negative values
}
}
fun main() {
val user = User()
user.age = 25 // Allowed: age becomes 25
user.age = -5 // Vetoed: age remains 25
}
Non-Null Properties (notNull)
The notNull delegate ensures a property is initialized before use, even if declared as non-null. It throws an exception if accessed before initialization (unlike nullable var with !!).
Use Case:
Properties that can’t be initialized in the constructor (e.g., Android Activity views).
Syntax:
var <propertyName>: <Type> by Delegates.notNull()
Example:
import kotlin.properties.Delegates
class Activity {
var textView: TextView by Delegates.notNull()
fun onCreate() {
textView = TextView(context) // Initialize later
}
}
fun main() {
val activity = Activity()
// activity.textView // Throws IllegalStateException: Property textView should be initialized before get.
activity.onCreate()
activity.textView // Safe to access
}
Map-Based Properties
The map delegate stores property values in a Map (or MutableMap for var), making it easy to work with dynamic data (e.g., JSON, CSV, or configuration files with unknown fields).
Syntax:
class <ClassName>(val map: Map<String, Any>) {
val <propertyName>: <Type> by map
}
Example (JSON-like data):
class User(map: Map<String, Any>) {
val name: String by map // Reads map["name"]
val age: Int by map // Reads map["age"]
val isStudent: Boolean by map // Reads map["isStudent"]
}
fun main() {
val userMap = mapOf(
"name" to "Alice",
"age" to 22,
"isStudent" to true
)
val user = User(userMap)
println(user.name) // Alice
println(user.age) // 22
}
For mutable properties, use MutableMap:
class MutableUser(map: MutableMap<String, Any>) {
var name: String by map
var age: Int by map
}
fun main() {
val userMap = mutableMapOf("name" to "Bob", "age" to 30)
val user = MutableUser(userMap)
user.age = 31 // Updates userMap["age"] to 31
println(userMap["age"]) // 31
}
Creating Custom Delegates
For unique use cases not covered by standard delegates, you can create custom delegates. A delegate is simply a class that implements getValue (and setValue for var).
Read-Only (val) Delegates
For val properties, the delegate must define a getValue method with the signature:
operator fun getValue(
thisRef: T, // The class instance owning the property (e.g., User)
property: KProperty<*> // Metadata about the property (name, type, etc.)
): R // The property type (e.g., String)
Mutable (var) Delegates
For var properties, add a setValue method:
operator fun setValue(
thisRef: T,
property: KProperty<*>,
value: R // The new value to set
)
Practical Examples of Custom Delegates
Example 1: Logging Delegate
Track when a property is accessed or modified:
class LoggingDelegate<T>(private var initialValue: T) {
// For val: getValue only
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Getting ${property.name} (value: $initialValue)")
return initialValue
}
// For var: add setValue
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
println("Setting ${property.name} from $initialValue to $value")
initialValue = value
}
}
// Usage
class User {
var name: String by LoggingDelegate("")
val id: Int by LoggingDelegate(123)
}
fun main() {
val user = User()
user.name = "Alice" // Setting name from to Alice
println(user.name) // Getting name (value: Alice) → Alice
println(user.id) // Getting id (value: 123) → 123
}
Example 2: Counter Delegate
Track how many times a property is accessed:
class CounterDelegate<T>(private val initialValue: T) {
private var count = 0
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
count++
println("${property.name} accessed $count times")
return initialValue
}
}
// Usage
class Stats {
val score: Int by CounterDelegate(100)
}
fun main() {
val stats = Stats()
stats.score // score accessed 1 times
stats.score // score accessed 2 times
}
Example 3: Persistent Delegate
Save property values to a file (e.g., for simple persistence):
import java.io.File
class PersistentDelegate(private val filePath: String) {
private var value: String? = null
// Load from file on first access
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
if (value == null) {
value = File(filePath).readText()
}
return value!!
}
// Save to file on update
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value
File(filePath).writeText(value)
}
}
// Usage: Persist "config.txt" content
class AppConfig {
var apiKey: String by PersistentDelegate("config.txt")
}
fun main() {
val config = AppConfig()
println(config.apiKey) // Reads from config.txt
config.apiKey = "NEW_KEY_123" // Writes to config.txt
}
Advanced Use Cases
Delegating to Another Property
You can delegate a property to another property (e.g., to expose a private property publicly):
class User {
private var _email: String = "[email protected]"
var publicEmail: String by ::_email // Delegate to _email
}
fun main() {
val user = User()
println(user.publicEmail) // [email protected]
user.publicEmail = "[email protected]"
println(user.publicEmail) // [email protected] (updates _email)
}
Generic Delegates with Reified Types
Use reified type parameters to create type-safe delegates without runtime overhead:
class TypeSafeDelegate<T : Any> {
private var value: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: error("${property.name} not initialized")
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
// Reified helper function for type inference
inline fun <reified T : Any> typeSafeDelegate() = TypeSafeDelegate<T>()
// Usage (no need to specify type explicitly)
class Example {
var number: Int by typeSafeDelegate()
var text: String by typeSafeDelegate()
}
Combining Delegates
Chain delegates to combine behavior (e.g., lazy + observable for lazy initialization with change tracking):
val lazyObservableValue: Int by lazy { 10 }.observable { _, old, new ->
println("Value changed from $old to $new")
}
fun main() {
println(lazyObservableValue) // 10 (initialized)
lazyObservableValue = 20 // Value changed from 10 to 20 (error! lazy is read-only)
}
Note: lazy returns a Lazy<T>, which is read-only. To combine with observable, use a var and wrap lazy in a mutable delegate.
Best Practices
- Prefer Standard Delegates First: Use
lazy,observable, etc., before writing custom delegates—they’re optimized and well-tested. - Keep Delegates Stateless When Possible: Stateless delegates (no internal state) are reusable across multiple properties.
- Document Behavior: Clearly document custom delegates (e.g., “Thread-safe?”, “Initialization cost?”).
- Avoid Overuse: For simple logic (e.g., a basic getter), use a regular property instead of a delegate.
- Test Edge Cases: For custom delegates, test initialization, concurrency, and error conditions (e.g.,
notNullbefore initialization).
Conclusion
Kotlin’s delegated properties are a versatile tool for encapsulating property logic, reducing boilerplate, and promoting code reuse. By leveraging built-in delegates like lazy and observable, or creating custom ones for unique needs, you can write cleaner, more maintainable code.
Start with standard delegates for common tasks, and when you encounter repetitive property logic, encapsulate it in a custom delegate. With practice, delegated properties will become an indispensable part of your Kotlin toolkit.