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.
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.
Exploring popular libraries
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.