Introduction

Briefly described, delegated properties are simply functions for delegating access (reading and writing) to a property using the by keyword. To implement it, you need to define getValue or setValueoperators for the class. To help and simplify the creation of these operators, you can use ready-made interfaces like ReadWriteProperty, ReadOnlyProperty or ready-made functions from kotlin stdlib: notNull , lazy , observable , vetoable . Then we’ll go deeper, and I advise you to look at the database in the official documentation .

Storage in a map and delegation to another property

1. The getter and setter of one property can be delegated to another property

The delegated property can be 
1 – top level property 
2 – class property or extension

To delegate a property to another property, use the syntax ::MyClass::delegateorthis::delegate

class MyClass(var myClassProperty: Boolean = false)
val clazz = MyClass()

var delegated by clazz::myClassProperty 
var notDelegated = clazz.myClassProperty

In this example , clazz::myClassPropertyit’s the same as clazz.myClassProperty, the only difference is that an additional instance is created for the notDelegated property, but not for delegated . This turns out to be a replacement for the manual definition of get() and set(value).

Complete delegation of access to a property, without additional logic, can rarely be useful in the office. doc gives an example of renaming and maintaining backward compatibility.

class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}
fun main() {
   val myClass = MyClass()
   // Notification: 'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

As an example from the official documentation, we mark an old property with an old name with the Deprecated annotation and all requests will now be redirected to a property with a new name.

2. Using a map instance as a delegate for storing properties

class Properties(map: Map<String, String>) {
    val name by map
    val version by map
}

val properties = Properties(
    mapOf(
        "name" to "delegate testing",
        "version" to "0.0.1"
    )
)

fun main() {
    println(properties.name) // delegate testing
    println(properties.version) // 0.0.1
}

Maps can often be returned by simply parsing a json response or some config with parameters, and using a delegated property in this case will make the code more readable. In the example above, we take values ​​from the map through string keys, which are property names.

Multiple receivers

For a single delegate we can have multiple getValue and  setValue with different arguments thisRef: ContextType, different method definitions will be called in different situations. This can be very useful; in the example below for Fragment and Activity, getValue will work differently, depending on the context.

class CachedPropertyDelegate {
  
    operator fun getValue(
        activity: Activity,
        prop: KProperty<*>
    ): String {
        return "delegated property from Activity"
    }

    operator fun getValue(
        fragment: Fragment,
        prop: KProperty<*>
    ): String {
        return "delegated property from Fragment"
    }
}

Extensions

The most non-obvious way to declare a delegate is to create an extension property, thanks to which we can take advantage of the advantage and brevity of delegates without cluttering our class with additional methods or classes in general.

class UserInfo(
    val name: String,
    val lastName: String
)

operator fun UserInfo.getValue(thisRef: Nothing?, property: KProperty<*>): String {
    val fullName = "$name $lastName"
    println("access to $fullName")
    return fullName
}

fun main() {
    val user = UserInfo("John", "Doe")
    val fullName by user
    println(fullName)
}
// output
// access to John Doe
// John Doe

Providers

For a delegate, you can override not only the getValue and  setValue operators , but also  provideDelegate . This function returns the instance of our delegate when defined with the keyword  by. By overriding this delegate, you can execute additional code, which can be very useful. There is also an auxiliary interfacePropertyDelegateProvider


@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class FileFormat(val fileFormat: String)

private val directoryPath get() = System.getProperty("user.dir") + "\\src\\main\\kotlin\\testdir"

class TextWriterDelegate(private val text: String) {
    private var file: File? = null

    operator fun getValue(thisRef: Nothing?, property: KProperty<*>): String {
        return file?.readText() ?: error("No such file")
    }

    operator fun provideDelegate(thisRef: Nothing?, property: KProperty<*>): TextWriterDelegate {
        val format = property.findAnnotation<FileFormat>()?.fileFormat?.let { ".$it" } ?: ".txt"
        val dir = File(directoryPath)
        dir.mkdir()
        file = File(dir, property.name + format)
        file?.writeText(text)
        return this
    }
}

fun texting(action: StringBuilder.() -> Unit) = TextWriterDelegate(buildString(action))

/**  */
@FileFormat("txt")
val appConfig by texting {
    val properties = listOf(
        "name" to "delegate testing",
        "version" to "0.0.1"
    )
    properties.forEach {
        appendLine("${it.first} = ${it.second}")
    }
}

In the example above, a file is created with the name property appConfig.txtand the lines of text that we wrote when declaring the delegate are written down.