Exploring Kotlin's Delegated Properties

In this tutorial, we will explore Kotlin's delegated properties, which allow us to delegate the implementation of property access and modification to another object. Delegated properties provide a powerful way to handle common scenarios such as lazy initialization, observable properties, handling null values, map properties, storing properties in preferences, and property delegation in libraries. By using delegated properties, we can write cleaner and more maintainable code.

exploring kotlins delegated properties

What are delegated properties?

Delegated properties in Kotlin allow us to delegate the implementation of property access and modification to another object. This means that instead of writing the getter and setter methods ourselves, we can delegate these operations to another object. Kotlin provides several built-in delegates such as lazy, observable, notNull, map, and preferences, which we can use to handle common scenarios.

Advantages of using delegated properties

The advantages of using delegated properties include:

  • Cleaner and more readable code: Delegated properties allow us to separate the implementation of property access and modification from the class that defines the property. This leads to cleaner and more readable code.
  • Reusability: By delegating property access and modification to another object, we can reuse the same implementation across multiple properties.
  • Easy to change the behavior: If we need to change the behavior of a property, we can simply change the delegate object instead of modifying the property access and modification logic in multiple places.

Lazy Initialization

Lazy initialization is a common scenario where we want to delay the initialization of a property until it is accessed for the first time. Kotlin provides a built-in delegate called lazy to handle lazy initialization.

Using 'lazy' delegate

The lazy delegate allows us to initialize a property lazily, meaning that the initialization code is only executed when the property is accessed for the first time. Here's an example:

val expensiveProperty: String by lazy {
    // Expensive initialization code
    // This code is only executed when the property is accessed for the first time
    "Hello, World!"
}

In this example, the property expensiveProperty is lazily initialized with the given lambda expression. The lambda expression is only executed when the property is accessed for the first time. The result of the lambda expression is then stored and returned whenever the property is accessed subsequently.

Custom lazy initialization

We can also define our own custom lazy initialization logic by implementing the Lazy interface. Here's an example:

class CustomLazy<T>(private val initializer: () -> T) : Lazy<T> {
    private var value: T? = null
    
    override val value: T
        get() {
            if (value == null) {
                value = initializer()
            }
            return value!!
        }
    
    override fun isInitialized(): Boolean = value != null
}

In this example, we define a custom lazy delegate by implementing the Lazy interface. The initializer lambda expression is provided during the delegate creation. The value property is lazily initialized with the given lambda expression. The isInitialized function is used to check whether the property has been initialized or not.

Observable Properties

Observable properties are properties that notify us when their value changes. Kotlin provides a built-in delegate called observable to handle observable properties.

Using 'observable' delegate

The observable delegate allows us to define observable properties. Here's an example:

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

In this example, the property name is defined as an observable property using the observable delegate. The lambda expression is called whenever the value of the property changes. The lambda expression receives three parameters: the property being changed, the old value, and the new value.

Custom observable properties

We can also define our own custom observable properties by implementing the ObservableProperty interface. Here's an example:

class CustomObservableProperty<T>(private var value: T) : ObservableProperty<T> {
    private val listeners = mutableListOf<(T, T) -> Unit>()
    
    override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
    
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        val oldValue = this.value
        this.value = value
        listeners.forEach { it(oldValue, value) }
    }
    
    override fun addListener(listener: (T, T) -> Unit) {
        listeners.add(listener)
    }
    
    override fun removeListener(listener: (T, T) -> Unit) {
        listeners.remove(listener)
    }
}

In this example, we define a custom observable property by implementing the ObservableProperty interface. The value property holds the current value of the observable property. The getValue function is used to get the value of the property. The setValue function is used to set the value of the property and notify the listeners. The addListener and removeListener functions are used to add and remove listeners.

NotNull Properties

NotNull properties are properties that cannot hold null values. Kotlin provides a built-in delegate called notNull to handle NotNull properties.

Using 'notNull' delegate

The notNull delegate allows us to define NotNull properties. Here's an example:

var name: String by Delegates.notNull()

In this example, the property name is defined as a NotNull property using the notNull delegate. If we try to access the property before it is initialized, a IllegalStateException will be thrown.

Handling null values

To handle null values, we can use the lateinit modifier along with the var keyword. Here's an example:

lateinit var name: String

In this example, the property name is declared as lateinit. This means that the property will be initialized later before it is accessed. If we try to access the property before it is initialized, a UninitializedPropertyAccessException will be thrown.

Map Properties

Map properties are properties that are backed by a map. Kotlin provides a built-in delegate called map to handle map properties.

Using 'map' delegate

The map delegate allows us to define map properties. Here's an example:

val user: Map<String, Any?> by mapOf(
    "name" to "John Doe",
    "age" to 30,
    "email" to "[email protected]"
)

In this example, the property user is defined as a map property using the map delegate. The map contains the key-value pairs where the key represents the property name and the value represents the property value.

Custom map properties

We can also define our own custom map properties by implementing the ReadWriteProperty interface. Here's an example:

class CustomMapProperty<T>(private val map: MutableMap<String, T>) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T = map[property.name]!!
    
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        map[property.name] = value
    }
}

In this example, we define a custom map property by implementing the ReadWriteProperty interface. The map property holds the key-value pairs of the map property. The getValue function is used to get the value of the property from the map. The setValue function is used to set the value of the property in the map.

Storing Properties in Preferences

Storing properties in preferences is a common scenario in Android development. Kotlin provides a built-in delegate called preferences to handle storing properties in preferences.

Using 'preferences' delegate

The preferences delegate allows us to store properties in preferences. Here's an example:

var name: String by PreferenceDelegate("name", "John Doe")

In this example, the property name is defined as a property that is stored in preferences using the preferences delegate. The PreferenceDelegate class is responsible for reading and writing the property value from/to preferences. The first parameter of the PreferenceDelegate constructor is the preference key, and the second parameter is the default value.

Custom preferences properties

We can also define our own custom preferences properties by implementing the ReadWriteProperty interface. Here's an example:

class CustomPreferencesProperty<T>(
    private val preferences: SharedPreferences,
    private val key: String,
    private val defaultValue: T
) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return preferences.run {
            when (defaultValue) {
                is Boolean -> getBoolean(key, defaultValue)
                is Int -> getInt(key, defaultValue)
                is Long -> getLong(key, defaultValue)
                is Float -> getFloat(key, defaultValue)
                is String -> getString(key, defaultValue)
                else -> throw IllegalArgumentException("Unsupported type")
            } as T
        }
    }
    
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        preferences.edit().apply {
            when (value) {
                is Boolean -> putBoolean(key, value)
                is Int -> putInt(key, value)
                is Long -> putLong(key, value)
                is Float -> putFloat(key, value)
                is String -> putString(key, value)
                else -> throw IllegalArgumentException("Unsupported type")
            }
        }.apply()
    }
}

In this example, we define a custom preferences property by implementing the ReadWriteProperty interface. The preferences property holds the SharedPreferences object. The key property holds the preference key. The defaultValue property holds the default value of the property. The getValue function is used to get the value of the property from preferences. The setValue function is used to set the value of the property in preferences.

Property Delegation in Libraries

Property delegation is a powerful feature that is widely used in Kotlin libraries. Many popular libraries provide their own delegated properties to handle common scenarios.

Some popular libraries that make use of property delegation include:

  • Anko: Anko is a library that provides a set of Kotlin extensions for Android development. It provides delegated properties for handling views, layouts, dialogs, and more.
  • Kotlin Android Extensions: Kotlin Android Extensions is a plugin for Android Studio that allows us to access views from XML layouts directly in Kotlin code. It provides delegated properties for accessing views.
  • Koin: Koin is a lightweight dependency injection framework for Kotlin. It provides delegated properties for injecting dependencies.
  • Exposed: Exposed is a lightweight SQL library for Kotlin. It provides delegated properties for defining database tables and executing SQL queries.

Examples of property delegation in libraries

Here are some examples of property delegation in libraries:

  • Anko:
val nameTextView: TextView by lazy { find<TextView>(R.id.nameTextView) }

In this example, the nameTextView property is defined as a delegated property using the find function from Anko. The find function is responsible for finding the view with the given ID.

  • Kotlin Android Extensions:
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        nameTextView.text = "Hello, World!"
    }
}

In this example, the nameTextView property is defined as a delegated property using Kotlin Android Extensions. The nameTextView property is automatically generated by the Kotlin Android Extensions plugin based on the XML layout file.

  • Koin:
class MyViewModel(private val userRepository: UserRepository) : ViewModel() {
    // Inject the UserRepository using Koin
    val users: List<User> by inject()
    
    fun getUserById(id: Int): User? {
        return userRepository.getUserById(id)
    }
}

In this example, the users property is defined as a delegated property using Koin. The users property is injected with the UserRepository dependency.

  • Exposed:
object Users : Table() {
    val id = integer("id").autoIncrement().primaryKey()
    val name = varchar("name", length = 50)
}

fun getUsers(): List<User> {
    return transaction {
        Users.selectAll().map { row ->
            User(row[Users.id], row[Users.name])
        }
    }
}

In this example, the Users table is defined as a delegated property using Exposed. The Users table is used to define the database schema. The getUsers function selects all the users from the Users table.

Conclusion

In this tutorial, we explored Kotlin's delegated properties and learned how to use them for lazy initialization, observable properties, handling null values, map properties, storing properties in preferences, and property delegation in libraries. Delegated properties provide a powerful way to handle common scenarios in a cleaner and more maintainable way. By using delegated properties, we can improve the readability and reusability of our code.