Understanding Kotlin Delegates with Practical Examples

If you write code in Kotlin, you’ve likely encountered the concept of Kotlin delegates, whether while using the by
keyword to initialize a variable with a lazy
block or obtaining a ViewModel instance via viewModels()
. These are classic examples of Kotlin delegates. In this post, I’ll walk you through Kotlin delegates with clear explanations and practical examples.
What Are Kotlin Delegates?
Kotlin delegates simplify and reuse code by allowing one object to handle certain tasks on behalf of another object. It’s like saying, “Hey, you take care of this part for me!”
What does it mean?
Imagine you have a class
A
with a propertyx
. You want to log every time a new value is assigned tox
or perform some custom tasks when it’s accessed or modified. While you can achieve this by adding custom logic directly in the getter and setter ofx
, it can quickly become repetitive and messy if you need to apply similar behavior to multiple properties. Instead of duplicating code, you can create a custom delegate class that handles this behavior. Then, you can reuse this delegate for any property in your class.
// Custom delegate class
class CustomLogDelegate<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 A {
var x: String by CustomLogDelegate("Initial Value")
var y: Int by CustomLogDelegate(0)
}
Here, the CustomLogDelegate handles logging for properties x
and y
. Kotlin also provides built-in delegate types and allows creating custom delegates.
Types of Delegation in Kotlin
- Property Delegation: Delegating property behavior to another object.
- Class Delegation: Delegating the implementation of an interface to another object.
Property Delegation
Property Delegation enables defining the behavior of a property in a separate class or function, reducing boilerplate code for repetitive tasks like getters and setters.
Syntax of Property Delegation
var <property-name>: <Type> by <delegate>
The by
keyword links the property to its delegate.
Built-in Property Delegates in Kotlin
Lazy Delegation
One of the most commonly used delegate in Kotlin that helps you initialize a property only when it’s accessed for the first time. The lazy delegate is a part of the Kotlin standard library and works by taking a lambda that provides the initialization logic. Once initialized, the value is cached and reused whenever the property is accessed again.
val propertyName: Type by lazy {
// Initialization logic here
}
Use cases for lazy:
- Expensive Operations: Use it for properties that involve heavy computation or data fetching.
- Optional Data: For properties that might not always be accessed during the lifecycle of an object.
- Thread-Safe Initialization: Ideal for properties shared across threads.
Scenarios where lazy is useful:
class TwitterClient {
private val client by lazy {
// Complex API setup only happens when first needed
RetrofitBuilder()
.setBaseUrl("https://api.twitter.com")
.setAuthToken(loadToken())
.build()
}
}
class Repository {
private val database by lazy {
// Expensive database connection only opened when needed
Room.databaseBuilder(context, AppDatabase::class.java, "mydb")
.build()
}
}
class ImageProcessor {
private val bitmap by lazy {
// Large image only loaded when needed
context.resources.getDrawable(R.drawable.large_image)
.toBitmap()
}
}
Observable Delegation
The observable delegation in Kotlin allows to monitor changes to a property and react whenever its value changes. The observable function is part of Kotlin’s standard library and provides a callback mechanism that gets invoked on every value change.
var propertyName: Type by observable(initialValue) { property, oldValue, newValue ->
// React to the change
}
Use cases for observable:
- Live Data Tracking: Monitor and update UI settings in real time
- Debugging: Track and log property changes
Scenarios where observable is useful:
class RegistrationForm {
var password: String by Delegates.observable("") { _, _, newValue ->
// Automatically validate password on change
passwordStrength = calculateStrength(newValue)
updatePasswordIndicator(passwordStrength)
}
var email: String by Delegates.observable("") { _, _, newValue ->
// Real-time email validation
isEmailValid = validateEmail(newValue)
updateEmailIndicator(isEmailValid)
}
}
class UserActivity {
var currentScreen: String by Delegates.observable("Home") { _, oldValue, newValue ->
// Log screen changes for analytics
analyticsTracker.logScreenChange(from = oldValue, to = newValue)
// Update user's last known location
updateUserLocation(newValue)
}
}
class ProductPrice {
var price by Delegates.observable(0.0) { _, oldPrice, newPrice ->
val difference = newPrice - oldPrice
when {
difference > 0 -> {
priceView.setTextColor(Color.RED)
showPriceChange("Price increased by $${difference}")
}
difference < 0 -> {
priceView.setTextColor(Color.GREEN)
showPriceChange("Price decreased by $${-difference}")
}
}
}
}
Vetoable Delegation
The vetoable delegation in Kotlin allows to monitor and potentially block changes to a property’s value. The vetoable function is also part of Kotlin’s standard library and provides a validation mechanism that lets enforce rules whenever the property is updated.
var propertyName: Type by vetoable(initialValue) { property, oldValue, newValue ->
// Decide whether to allow the change
conditionToAllowChange
}
Use cases for vetoable:
- Validation: Ensure a property’s value meets specific criteria before accepting changes.
- Restricting Updates: Prevent undesired or unsafe modifications to critical properties.
Scenarios where vetoable is useful:
class UserSettings {
var theme: String by Delegates.vetoable("Light") { _, _, newValue ->
// Allow only specific themes
val allowedThemes = listOf("Light", "Dark")
if (newValue in allowedThemes) {
println("Theme updated to $newValue")
true
} else {
println("Invalid theme: $newValue")
false
}
}
}
class BankAccount {
var balance: Double by Delegates.vetoable(0.0) { _, oldValue, newValue ->
// Prevent balance from going negative
if (newValue < 0) {
println("Invalid operation: Balance cannot be negative.")
false
} else {
println("Balance updated from $oldValue to $newValue")
true
}
}
}
class UserProfile {
var age: Int by Delegates.vetoable(18) { _, oldValue, newValue ->
// Allow only valid age range
if (newValue in 13..100) {
println("Age updated from $oldValue to $newValue")
true
} else {
println("Invalid age: $newValue. Keeping $oldValue.")
false
}
}
var username: String by Delegates.vetoable("Guest") { _, _, newValue ->
// Enforce username validation
if (newValue.matches("^[a-zA-Z0-9_]{3,15}$".toRegex())) {
println("Username updated to $newValue")
true
} else {
println("Invalid username: $newValue. Must be 3-15 alphanumeric characters.")
false
}
}
}
Map Delegation
The map delegation in Kotlin allows to delegate property storage to a Map object. This is particularly useful for working with dynamic or external data sources, like JSON objects or database rows, where property values are stored and retrieved using keys.
val propertyMap = mapOf("key1" to value1, "key2" to value2)
val key1: Type by propertyMap
val key2: Type by propertyMap
Use cases for map delegation:
- JSON Parsing: Simplify access to fields in JSON objects retrieved from REST APIs by mapping keys to properties directly.
- Intent or Bundle Data: Simplify extracting values from Android Intent extras or Bundle objects, making the code cleaner and more maintainable.
Scenarios where map delegation is useful:
class Product(private val attributes: Map<String, Any>) {
val productId: Int by attributes
val productName: String by attributes
val price: Double by attributes
}
val productData = mapOf("productId" to 501, "productName" to "Laptop", "price" to 1500.0)
val product = Product(productData)
private val data: Map<String, Any?> by lazy {
// `keySet()` retrieves all the keys, and `associateWith` maps each key to its corresponding value.
intent.extras?.keySet()?.associateWith { intent.extras?.get(it) } ?: emptyMap()
}
val title: String? by data
val itemId: Int? by data
private val sharedPrefs = context.getSharedPreferences("UserPrefs", Context.MODE_PRIVATE)
private val prefsMap: Map<String, Any?> by lazy {
sharedPrefs.all // Get all stored preferences as a map
}
val isDarkMode: Boolean by prefsMap
val username: String? by prefsMap
Delegating to Another Property
Kotlin also always to delegate the getter and setter of a property to another property. This feature is available for both top-level and class properties, including member and extension properties. To delegate a property, use the :: qualifier with the name of the delegate property, such as this::delegate or MyClass::delegate.
var propertyName: Type by this::delegatePropertyName
Use Cases for Delegating to Another Property:
- Backward Compatibility: Gradually transition from an old property to a new one while maintaining compatibility with older versions of the codebase.
- Streamlining Updates: Maintain a single source of truth for property values across different properties.
Scenarios where delegating to another property is useful:
class Employee {
// New property for storing the current role
var currentRole: String = "Junior Developer"
// Deprecated property delegating to the new one
@Deprecated("Use 'currentRole' instead", ReplaceWith("currentRole"))
var previousRole: String by this::currentRole
}
fun main() {
val employee = Employee()
// Using the deprecated property
// Notification: 'previousRole: String' is deprecated. Use 'currentRole' instead.
employee.previousRole = "Senior Developer"
// The new property reflects the change
println(employee.currentRole) // Output: Senior Developer
}
Custom Property Delegates in Kotlin
Let’s explore how to map JSON keys directly to Kotlin properties with a custom property delegates. First create a custom extension function to delegating JSON properties.
operator fun <T> JSONObject.getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): T =
this.get(property.name) as T
- Operator Overloading: The
operator
keyword allowsgetValue
to be used with theby
keyword, enabling seamless property delegation. - Property Mapping: The
property
parameter provides the name of the delegated property, which is used to fetch the corresponding value from theJSONObject
usingthis.get(property.name)
. - Generic Return Type: The
<T>
makes the function flexible, allowing it to return values of any type (e.g.,String
,Double
) by casting the JSON value to the expected type.
Finally this function maps JSON keys directly to Kotlin properties using the by
keyword, simplifying JSON parsing with a clean, reusable pattern.
class Product(json: JSONObject) {
val id: String by json
val name: String by json
val price: Double by json
}
val json = JSONObject("""{"id": "123", "name": "Smartphone", "price": 799.99}""")
val product = Product(json)
println(product.name) // Output: Smartphone
Let’s create another simple reusable custom property delegate to handle reading and writing SharedPreferences properties.
// Basic implementation of a custom sharedPreferences delegate
class SharedPreferencesDelegate<T>(
private val sharedPreferences: SharedPreferences,
private val key: String,
private val defaultValue: T
) {
@Suppress("UNCHECKED_CAST")
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return when (defaultValue) {
is String -> sharedPreferences.getString(key, defaultValue) as T
is Int -> sharedPreferences.getInt(key, defaultValue) as T
is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T
is Float -> sharedPreferences.getFloat(key, defaultValue) as T
is Long -> sharedPreferences.getLong(key, defaultValue) as T
else -> throw IllegalArgumentException("Unsupported type")
}
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
with(sharedPreferences.edit()) {
when (value) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
is Long -> putLong(key, value)
else -> throw IllegalArgumentException("Unsupported type")
}
apply()
}
}
}
// Create a Wrapper Class
class AppPreferences(context: Context) {
private val sharedPreferences =
context.getSharedPreferences("AppPreferences", Context.MODE_PRIVATE)
var username: String by SharedPreferencesDelegate(sharedPreferences, "username", "Guest")
var isLoggedIn: Boolean by SharedPreferencesDelegate(sharedPreferences, "isLoggedIn", false)
var highScore: Int by SharedPreferencesDelegate(sharedPreferences, "highScore", 0)
}
// Use the AppPreferences class in Android activity:
class MainActivity : AppCompatActivity() {
private lateinit var preferences: AppPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize AppPreferences
preferences = AppPreferences(this)
// Save data to SharedPreferences
preferences.username = "JohnDoe"
preferences.isLoggedIn = true
preferences.highScore = 1200
// Retrieve and display data from SharedPreferences
println("Username: ${preferences.username}")
println("Is Logged In: ${preferences.isLoggedIn}")
println("High Score: ${preferences.highScore}")
}
}
Class Delegation
Class delegation is a powerful feature in Kotlin that enables a class to delegate the implementation of an interface to another object. This promotes code reusability and simplifies code by separating concerns, allowing to focus on composition over inheritance.
Composition over inheritance means creating software by combining small, reusable pieces instead of using class hierarchies, making code more flexible and easier to maintain.
// Define a generic interface
interface BaseInterface {
fun doSomething(): String
}
// Concrete implementation of the interface
class ConcreteImplementation : BaseInterface {
override fun doSomething(): String {
return "Doing something in ConcreteImplementation"
}
}
// Delegating class that delegates functionality to another object
class DelegatingClass(delegate: BaseInterface) : BaseInterface by delegate
fun main() {
val realImplementation = ConcreteImplementation()
// Create a delegating object
val delegatingObject = DelegatingClass(realImplementation)
// Call the function via the delegating object
println(delegatingObject.doSomething()) // Output: Doing something in ConcreteImplementation
}
Here the Base Interface defines the contract, the Concrete Implementation provides the functionality, and the Delegating Class uses by to automatically forward all functions to the provided implementation.
Use cases for class delegation:
- Reusable Logic: Class delegation lets reuse code by passing responsibilities to another class instead of repeating the code in subclasses.
- Composition Over Inheritance: It offers a cleaner alternative to inheritance, helping create modular classes that focus on composition rather than subclassing.
- Decouple Code: Delegation reduces tight coupling by allowing a class to delegate some of its tasks to another class, making code more flexible and easier to maintain.
Scenarios where class delegation is useful:
//Define Multiple Interfaces for Delegation
interface LoadingState {
fun showLoading(isLoading: Boolean)
}
interface ErrorHandler {
fun handleNetworkError(error: String)
}
interface UserInteraction {
fun handleUserClick(action: String)
}
// Implement the Interfaces
class LoadingStateImpl : LoadingState {
override fun showLoading(isLoading: Boolean) {
if (isLoading) {
println("Loading...")
} else {
println("Loading finished.")
}
}
}
class ErrorHandlerImpl : ErrorHandler {
override fun handleNetworkError(error: String) {
println("Network Error: $error")
}
}
class UserInteractionImpl : UserInteraction {
override fun handleUserClick(action: String) {
println("User clicked on: $action")
}
}
//Delegate Only Necessary Behaviors in the Activity
class MainActivity : AppCompatActivity(), LoadingState by LoadingStateImpl(),
ErrorHandler by ErrorHandlerImpl(), UserInteraction by UserInteractionImpl() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Show loading state
showLoading(true)
// Simulate a network error
handleNetworkError("Failed to fetch data")
// Handle user click action
handleUserClick("Submit Button")
}
}
//Delegate Only Necessary Behaviors in the Activity
class AnotherActivity : AppCompatActivity(), LoadingState by LoadingStateImpl(),
ErrorHandler by ErrorHandlerImpl(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_another)
// Show loading
showLoading(true)
// Simulate a network error
handleNetworkError("Connection timeout")
}
}
Unlike base class inheritance, class delegation enables you to share behavior without tightly coupling activities to a common base class. It provides greater flexibility and separation of concerns, keeping your code clean, modular, and easier to extend.
Kotlin Delegates are a great way to simplify your code and improve readability. Explore the built-in and custom delegates to enhance your Kotlin projects. Feel free to reach out and share your thoughts on this article or any other Kotlin-related topics! You can connect with me on LinkedIn — let’s keep the conversation going!