In this blog post, we’ll dive deep into delegation in Kotlin. If you’re a mid-level developer familiar with Kotlin basics but wanting to level up your understanding of advanced features, this is for you. We’ll cover how delegation works at the language level, explore class and property delegation, dissect built-in delegates, create custom ones, and even discuss performance implications and when to avoid it. Expect clear explanations, full code examples, and practical insights to help you apply this in your projects.
By the end, you’ll see why delegation is a cornerstone of Kotlin’s design philosophy: making code concise, safe, and expressive. Let’s get started!
Delegation as a Language Feature
Delegation in Kotlin is fundamentally about composition. In object-oriented programming, we often face the choice between inheritance (extending a class) and composition (using objects to build behavior). Inheritance can lead to tight coupling and the “fragile base class” problem, where changes in a superclass break subclasses. Composition, on the other hand, allows you to assemble behaviors from reusable components.
Kotlin elevates composition to a language feature through delegation. Instead of manually forwarding calls to composed objects (which is boilerplate-heavy in Java), Kotlin handles it automatically via the by keyword for classes and delegates for properties. This reduces code duplication and makes your intent clearer.
At its core, delegation lets one object (the delegate) handle responsibilities on behalf of another (the delegator). This aligns with the Delegation Pattern from design patterns literature, but Kotlin makes it first-class.
Why is this useful? Imagine building a UI component that needs to handle clicks, but you don’t want to inherit from a base click handler class. Instead, you compose a click handler object and delegate the handling to it. Kotlin generates the glue code for you.
Delegation promotes the “favor composition over inheritance” principle from Effective Java. In Kotlin, it’s not just advice—it’s enforced with syntax sugar.
To illustrate, consider a simple non-delegated example in Java for contrast:
Java
public class MyClickHandler implements ClickListener {
private ClickListener delegate;
public MyClickHandler(ClickListener delegate) {
this.delegate = delegate;
}
@Override
public void onClick() {
delegate.onClick(); // Manual forwarding
}
}
In Kotlin with delegation, it’s effortless:
Kotlin
interface ClickListener {
fun onClick()
}
class MyClickHandler(delegate: ClickListener) : ClickListener by delegate
Here, MyClickHandler implements ClickListener by delegating all methods to the provided delegate. No manual forwarding needed—Kotlin’s compiler generates it.
This feature shines in scenarios like decorators, proxies, or when mixing behaviors from multiple sources without multiple inheritance (which Kotlin doesn’t support, like Java).
As a mid-level dev, you might wonder: When to use it? Use delegation when you need to reuse behavior without subclassing, especially for interfaces. It’s perfect for Android’s lifecycle-aware components or Retrofit’s API interfaces.
In the next sections, we’ll break it down further.
(Word count so far: ~450)
Class Delegation (by) and Code Generation
Class delegation uses the by keyword to delegate interface implementations to another object. This is particularly powerful because Kotlin allows multiple interface delegations in one class, simulating mixin-like behavior.
Let’s dissect how it works. When you write class MyClass(delegate: SomeInterface) : SomeInterface by delegate, the compiler generates implementations for all methods in SomeInterface that forward to the delegate. If you override a method in MyClass, your implementation takes precedence.
Full example: Suppose we have a Logger interface and a BaseLogger implementation. We want a TimedLogger that adds timing without inheriting.
Kotlin
interface Logger {
fun log(message: String)
}
class BaseLogger : Logger {
override fun log(message: String) {
println("Log: $message")
}
}
class TimedLogger(private val delegate: Logger) : Logger by delegate {
override fun log(message: String) {
val start = System.currentTimeMillis()
delegate.log(message) // We can still call delegate explicitly if needed
val end = System.currentTimeMillis()
println("Time taken: ${end - start}ms")
}
}
fun main() {
val logger = TimedLogger(BaseLogger())
logger.log("Hello, Delegation!")
}
Output:
text
Log: Hello, Delegation!
Time taken: 0ms // Approximate
Here, TimedLogger delegates to BaseLogger but overrides log to add timing. Without the override, it would just forward.
What about code generation? Under the hood, Kotlin compiles this to bytecode similar to manual forwarding. Using tools like IntelliJ’s “Show Kotlin Bytecode,” you can see it generates methods like:
Java
// Simplified decompiled Java
public class TimedLogger implements Logger {
private final Logger delegate;
public TimedLogger(Logger delegate) {
this.delegate = delegate;
}
@Override
public void log(String message) {
// Your override code here
}
// If no override, it would be: delegate.log(message);
}
For interfaces with multiple methods, delegation saves you from writing boilerplate for each.
You can delegate multiple interfaces:
Kotlin
interface Reader {
fun read(): String
}
interface Writer {
fun write(data: String)
}
class FileReader : Reader {
override fun read() = "Data from file"
}
class FileWriter : Writer {
override fun write(data: String) {
println("Writing: $data")
}
}
class FileHandler(r: Reader, w: Writer) : Reader by r, Writer by w
fun main() {
val handler = FileHandler(FileReader(), FileWriter())
println(handler.read())
handler.write("New data")
}
This composes Reader and Writer behaviors seamlessly.
As a mid-level dev, note: Delegation only works for interfaces, not classes (Kotlin doesn’t support class delegation directly). Also, the delegate must implement the interface.
Common pitfalls: If the delegate changes at runtime, delegation won’t reflect that since it’s compile-time. For dynamic delegation, use composition manually.
This mechanism is key for libraries like Kotlin Coroutines, where CoroutineScope often uses delegation.
(Word count so far: ~950)
Property Delegates: Contract and Lifecycle
Shifting to properties: Property delegation allows you to delegate the getter and setter of a property to another object. This is done with by on properties, like val myProp: Type by Delegate().
The contract: A property delegate must provide getValue and/or setValue functions, depending on val or var. These are operator functions from kotlin.properties package.
For a read-only property (val):
Kotlin
operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T
For mutable (var):
Kotlin
operator fun <T> setValue(thisRef: Any?, property: KProperty<*>, value: T)
thisRef is the instance owning the property, property is metadata about the property (name, etc.), useful for logging or dynamic behavior.
Lifecycle: Delegates are initialized when the property is first accessed (lazy by default? No, depends on the delegate). For top-level or class properties, the delegate object is created at initialization time, but its logic runs on access.
Example skeleton:
Kotlin
import kotlin.reflect.KProperty
class SimpleDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Value from delegate for ${property.name}"
}
}
class User {
val name: String by SimpleDelegate()
}
fun main() {
val user = User()
println(user.name) // "Value from delegate for name"
}
Here, SimpleDelegate provides the value. The lifecycle starts when User is instantiated (delegate created), but getValue is called on first access to name.
For mutable:
Kotlin
class MutableDelegate {
private var stored: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = stored
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
stored = value.uppercase()
}
}
class Product {
var title: String by MutableDelegate()
}
fun main() {
val product = Product()
product.title = "kotlin book"
println(product.title) // "KOTLIN BOOK"
}
The contract ensures type safety: The delegate’s functions must match the property type.
As a mid-level dev, understand that delegates can hold state, making them powerful for caching, validation, etc. They’re resolved at compile-time, so no runtime overhead for resolution.
Delegates can be reused across properties, promoting DRY (Don’t Repeat Yourself).
(Word count so far: ~1400)
Built-in Delegates (lazy, observable, vetoable)
Kotlin provides several out-of-the-box delegates in kotlin.properties.Delegates.
First, lazy: Defers computation until first access. Thread-safe by default (synchronized).
Kotlin
class ExpensiveResource {
val heavyData: List<Int> by lazy {
println("Computing heavy data...")
(1..1000000).toList() // Simulate expensive op
}
}
fun main() {
val res = ExpensiveResource()
println("Object created") // No computation yet
println(res.heavyData.size) // Computes now
println(res.heavyData.size) // Reuses
}
Output:
text
Object created
Computing heavy data...
1000000
1000000
You can customize laziness with LazyThreadSafetyMode.NONE for no sync, or PUBLICATION for publication idiom.
Next, observable: Watches changes to a mutable property.
Kotlin
import kotlin.properties.Delegates
class ObservableUser {
var age: Int by Delegates.observable(0) { property, old, new ->
println("${property.name} changed from $old to $new")
}
}
fun main() {
val user = ObservableUser()
user.age = 25 // "age changed from 0 to 25"
user.age = 30 // "age changed from 25 to 30"
}
The handler receives property metadata, old, and new values.
vetoable: Like observable, but can veto changes by returning false.
Kotlin
class VetoableUser {
var age: Int by Delegates.vetoable(0) { property, old, new ->
if (new >= 0) true else {
println("Invalid age: $new")
false
}
}
}
fun main() {
val user = VetoableUser()
user.age = 25 // Accepted
user.age = -5 // "Invalid age: -5" - not set
println(user.age) // 25
}
These are great for reactive programming, validation, or lazy loading in UI (e.g., Android ViewModels).
Combine them: var prop by Delegates.observable(lazy { … }.value) { … } – but better to use custom if complex.
(Word count so far: ~1800)
Custom Property Delegates
Creating custom delegates lets you encapsulate common property behaviors. Define a class implementing ReadOnlyProperty or ReadWriteProperty interfaces, or just provide the operator functions.
For reusability, use classes.
Example: A delegate that caches values from a map, with default.
Kotlin
import kotlin.reflect.KProperty
class MapDelegate(private val map: MutableMap<String, String>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return map.getOrDefault(property.name, "Default")
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
map[property.name] = value
}
}
class Config {
private val storage = mutableMapOf<String, String>()
var apiKey: String by MapDelegate(storage)
var endpoint: String by MapDelegate(storage)
}
fun main() {
val config = Config()
println(config.apiKey) // "Default"
config.apiKey = "secret"
println(config.apiKey) // "secret"
}
This delegates storage to a map, useful for configs or prefs.
For generics, make it templated:
Kotlin
class GenericDelegate<T>(private val init: () -> T) {
private var value: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (value == null) {
value = init()
}
return value!!
}
}
class LazyGeneric {
val data: List<String> by GenericDelegate { listOf("a", "b") }
}
This is a simple lazy without stdlib.
Advanced: Delegates with parameters. Use a function returning the delegate.
Kotlin
fun <T> cached(init: () -> T): ReadOnlyProperty<Any?, T> = object : ReadOnlyProperty<Any?, T> {
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (value == null) value = init()
return value!!
}
}
class CachedUser {
val profile: String by cached { "Loaded profile" }
}
This factory pattern allows parameterization.
As mid-level, experiment with delegates for logging, thread-safety, or integration with libs like Room or SharedPreferences.
(Word count so far: ~2200)
Delegates and Backing Fields
Backing fields are the private fields storing property values. With delegates, there’s no automatic backing field—the delegate handles storage.
In a normal property:
Kotlin
var x: Int = 0
get() = field
set(value) { field = value }
field is the backing field.
With delegates, you control storage inside the delegate. If you need a backing field in a delegate, manage it yourself.
Example: A delegate with backing:
Kotlin
class BackingDelegate {
private var backing: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = backing
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
backing = value
}
}
Here, backing is like a manual backing field.
Delegates can access the property’s metadata to decide storage, e.g., per-property backing in a map.
Important: Local variables can use delegates too, but no backing field needed since locals are simple.
Kotlin
fun main() {
val local by lazy { "Local lazy" }
println(local)
}
For performance, delegates might add indirection, but often negligible.
If you need to mix custom accessors with delegates, you can’t directly—delegates replace get/set. Solution: Delegate to something that calls your logic.
(Word count so far: ~2450)
Performance and Bytecode Implications
Delegation is compile-time, so no reflection overhead. But it adds method calls.
For class delegation: Each delegated method is a forwarder, like an extra invoke. In hot paths, this might matter, but JVM inlines small methods.
Bytecode for class delegation: Similar to Java interfaces, with forwarder methods.
For properties: getValue/setValue are called on access. Built-in like lazy use atomic for sync, adding minor cost.
Benchmark example (simplified):
Use JMH or simple loops. Lazy might save time on unused props, but first access pays init cost.
Custom delegates: Avoid heavy ops in get/set.
Bytecode: Use javap or IDE. A lazy prop becomes:
Java
private final Lazy lazy$delegate = LazyKt.lazy(() -> { ... });
public String getMyProp() {
return (String) lazy$delegate.getValue();
}
It’s a field holding the Lazy instance, and getter calls getValue.
Implications: More objects (delegates), potential GC pressure. But for most apps, fine.
Optimize: Use lazy(LazyThreadSafetyMode.NONE) for single-thread.
In Android, delegates help with memory—lazy for views.
Overall, benefits outweigh costs unless profiling shows otherwise.
(Word count so far: ~2700)
When Delegation Becomes a Liability
Delegation isn’t always ideal. Overuse can obscure code: If everything is delegated, tracing behavior is hard.
Performance: In tight loops, extra indirection hurts. Prefer direct impl.
Complexity: Custom delegates can become mini-frameworks—keep simple.
When inheritance fits better: For deep hierarchies, inheritance might be clearer (but rare in Kotlin).
Debugging: Stack traces show forwarders, hiding origins.
Alternatives: Extension functions, composition without by.
Use when: Reusing impl, avoiding inheritance.
Avoid when: Simple cases, or if delegate changes dynamically (use wrappers).
In summary, delegation empowers composition, but wield wisely.
