Kotlin Delegated Properties: Reduce Boilerplate Code

Home » Kotlin » Kotlin Delegated Properties: Reduce Boilerplate Code

Kotlin, known for its concise syntax and powerful features, simplifies common programming tasks. One such feature is Kotlin Delegated Properties. Delegated properties in Kotlin allow you to delegate the responsibility of property access and storage to another object. This can significantly reduce boilerplate code while enhancing flexibility and functionality.

In this tutorial, we’ll explore how delegated properties work, when to use them, and examine practical examples that showcase their versatility.

Kotlin Delegated Properties: Reduce Boilerplate Code

What Are Kotlin Delegated Properties?

Kotlin delegated properties are one of the language’s most powerful yet under-appreciated features. They allow you to extract common property behaviors and reuse them across different classes, following the delegation pattern. Instead of implementing the same property logic repeatedly, you can delegate that responsibility to helper objects.

In Kotlin, a delegated property allows a separate object (the delegate) to handle the property’s getter and setter logic. Instead of managing the property manually, you delegate its implementation. Kotlin provides built-in support for property delegation with the by keyword.

Understanding Delegated Properties

A delegated property is a property whose getter (and optionally setter) is outsourced to another object, called the delegate. The delegate handles the property’s storage and provides the logic for accessing and modifying its value.

The basic syntax looks like this:

Open Kotlin play ground and write the following program.

import kotlin.reflect.KProperty
class Example {
    var property: String by Delegate()
}

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Hi BigKnol : Provides this value!"
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("Hi BigKnol : setValue stores the value: $value")
    }
}

fun main() {
    val example = Example()
    println(example.property) 
    example.property = "New Value" // Prints: setValue stores the value: New Value
}

Output

Hi BigKnol : Provides this value!
Hi BigKnol : setValue stores the value: New Value

The by keyword is what connects the property with its delegate.

Standard Delegates in Kotlin

Kotlin’s standard library includes several ready-to-use delegates:

Lazy Properties

The lazy delegate allows you to compute a value only when it’s first accessed:

class ExpensiveResource {
    val data: String by lazy {
        println("Computing expensive data...")
        // Simulate expensive operation
        Thread.sleep(1000)
        "Expensive data result"
    }
}

fun main() {
    val resource = ExpensiveResource()
    println("Resource created, but data not yet computed")
    
    // First access triggers computation
    println("First access: ${resource.data}")
    
    // Subsequent accesses use cached value
    println("Second access: ${resource.data}")
}

Output

Resource created, but data not yet computed
Computing expensive data...
First access: Expensive data result
Second access: Expensive data result

The lazy delegate is thread-safe by default, making it suitable for concurrent applications.

Observable Properties

The observable delegate lets you react to property changes:

import kotlin.properties.Delegates

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

fun main() {
    val user = User()
    user.name = "Nikin"
    user.name = "Hari"
}


Output


name changed from 'Initial' to 'Nikin'
name changed from 'Nikin' to 'Hari'

Vetoable Properties

The vetoable delegate lets you validate property changes before they happen:

import kotlin.properties.Delegates

class User {
    var age: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
        if (newValue >= 0) {
            println("Age changed from $oldValue to $newValue")
            true  // Accept the change
        } else {
            println("Rejecting negative age: $newValue")
            false  // Reject the change
        }
    }
}

fun main() {
    val user = User()
    user.age = 25
    println("Current age: ${user.age}")
    
    user.age = -5
    println("Current age: ${user.age}")
    
    user.age = 30
    println("Current age: ${user.age}")
}

Output

Age changed from 0 to 25
Current age: 25
Rejecting negative age: -5
Current age: 25
Age changed from 25 to 30
Current age: 30

Map-Based Delegation

You can delegate properties to a map, which is helpful for dynamic objects:

class EnvironmentConfig(private val map: Map<String, Any?>) {
    val apiKey: String by map
    val timeout: Int by map
    val baseUrl: String by map
}

fun main() {
    val config = EnvironmentConfig(mapOf(
        "apiKey" to "abc123xyz",
        "timeout" to 30000,
        "baseUrl" to "https://api.example.com"
    ))
    
    println("API Key: ${config.apiKey}")
    println("Timeout: ${config.timeout}")
    println("Base URL: ${config.baseUrl}")
}

Output

API Key: abc123xyz
Timeout: 30000
Base URL: https://api.example.com

Creating Custom Property Delegates

Custom delegates give you complete control over property behavior. Let’s create a delegate that validates email addresses:

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

class EmailValidator : ReadWriteProperty<Any?, String> {
    private var email: String = ""
    
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return email
    }
    
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        if (!value.matches(Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$"))) {
            throw IllegalArgumentException("Invalid email format: $value")
        }
        email = value
    }
}

class User {
    var email: String by EmailValidator()
}

fun main() {
    val user = User()
    
    try {
        user.email = "invalid-email"
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
    
    user.email = "user@example.com"
    println("Valid email set: ${user.email}")
    
    try {
        user.email = "another@invalid"
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
}

Output

Error: Invalid email format: invalid-email
Valid email set: user@example.com
Error: Invalid email format: another@invalid

Kotlin Advanced Use Case: Cached Properties

Let’s implement a cache system using delegated properties:

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

class CachedProperty<in R, out T>(private val compute: () -> T) : ReadOnlyProperty<R, T> {
    private var value: T? = null
    private var initialized = false
    
    override fun getValue(thisRef: R, property: KProperty<*>): T {
        if (!initialized) {
            println("Computing value for ${property.name}...")
            value = compute()
            initialized = true
        } else {
            println("Returning cached value for ${property.name}")
        }
        return value as T
    }
    
    fun invalidate() {
        initialized = false
        value = null
    }
}

class DataRepository {
    val users: List<String> by CachedProperty {
        // Simulate database query
        println("Fetching users from database...")
        Thread.sleep(1000)
        listOf("Alice", "Bob", "Charlie")
    }
    
    val userCount: Int by CachedProperty {
        println("Calculating user count...")
        users.size  // Uses the cached users list
    }
    
    fun clearCache() {
        (this::users as? CachedProperty<*, *>)?.invalidate()
        (this::userCount as? CachedProperty<*, *>)?.invalidate()
    }
}

fun main() {
    val repo = DataRepository()
    
    // First access computes values
    println("Users: ${repo.users}")
    println("User count: ${repo.userCount}")
    
    // Second access uses cached values
    println("\nRetrieving again:")
    println("Users: ${repo.users}")
    println("User count: ${repo.userCount}")
    
    // Clear cache and fetch again
    println("\nClearing cache and retrieving again:")
    repo.clearCache()
    println("Users: ${repo.users}")
    println("User count: ${repo.userCount}")
}

Output

Computing value for users...
Fetching users from database...
Users: [Alice, Bob, Charlie]
Computing value for userCount...
Calculating user count...
Returning cached value for users
User count: 3

Retrieving again:
Returning cached value for users
Users: [Alice, Bob, Charlie]
Returning cached value for userCount
User count: 3

Clearing cache and retrieving again:
Returning cached value for users
Users: [Alice, Bob, Charlie]
Returning cached value for userCount
User count: 3

Practical Use Cases for Delegated Properties

  • Form validation: Create validators for different input types
  • Configuration management: Load configurations dynamically
  • Database access: Lazy-load database records only when needed
  • Caching: Cache expensive computations until invalidated
  • Dependency injection: Inject dependencies into properties
  • Thread safety: Ensure safe property access in concurrent environments
  • Binding UI elements: Connect UI components to data models

Performance Considerations

While delegated properties offer great flexibility, they come with a small overhead:

  • Each property access involves extra function calls
  • Reflection is used in some delegates (like observable and vetoable)
  • Creating many delegate instances can increase memory usage

For most applications, this overhead is negligible, but in performance-critical sections, direct property access might be preferred.

Best Practices

  • Use standard delegates when possible rather than creating custom ones
  • Document your delegates clearly, especially custom ones
  • Keep delegates focused on a single responsibility
  • Consider thread safety when designing delegates for concurrent access
  • Test edge cases thoroughly, as delegates can introduce subtle bugs
  • Be cautious with recursive property access within delegates

Conclusion

Kotlin delegated properties offer a powerful way to extract and reuse common property behaviors. Whether you’re implementing lazy initialization, validation, or dynamic binding, delegates can help make your code more maintainable and concise.

Remember that like any powerful feature, delegates should be used thoughtfully. They’re most valuable when they help encapsulate complex property logic that would otherwise be duplicated across multiple classes.


What delegated property pattern will you implement in your next Kotlin project?

Happy Coding!

You may also like...