DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Marc Reichelt
Marc Reichelt

Posted on

Make your code beautiful with Kotlin Property Delegates

My colleague Andreas Feldschmid and I are about to prepare advanced Kotlin topics for the second day of a two-day Kotlin training. It's pretty amazing that we are now Kotlin trainers at our company iteratec - and I'll write another blogpost about how that came to be in the upcoming weeks. By the way: if there is anything that you would love to learn in an advanced Kotlin training: please leave a comment below or write me on Twitter! πŸ“©

In order to select topics for this training, we were talking about lots of Kotlin features and libraries. And one topic that came up were Delegated Properties. He heard about them, but didn't use them yet. So I quickly opened IntelliJ and showed him this example.

Small recap on properties

This is pretty standard: In Kotlin, we can define mutable properties with var and values with val. Let's see some code:

class Preferences {
    var username : String = ""
}

Here, we have a property username. And each mutable property has a getter and a setter, which you can override. By the way, if you don't know the syntax by memory, you can simply type Alt + Enter on username and get the quick action for that:

Type Alt + Enter on a property in IntelliJ to add getter and setter methods

There's more to read about that in the Kotlin docs on properties. But instead of implementing properties yourself, you can instead create delegated properties, where the getter and setter are provided someplace else.

Awesome code with delegated properties

A delegated property, as described in the Kotlin docs is simply defined by using the by keyword and a delegate instance:

class Preferences {
    var username : String by Delegate()
}

This can be any class that implements the operator functions getValue and setValue. You can copy those from the documentation - but you can simply let IntelliJ create that class for you. And there are interfaces ReadWriteProperty and ReadOnlyProperty available which can help you with the syntax as well.

As an example, we'll create a FileDelegate that reads and writes text to a file, and takes a file name as parameter:

class Preferences {
    var username: String by FileDelegate("username.txt")
}

class FileDelegate(val fileName: String) : ReadWriteProperty<Preferences, String> {

    override fun getValue(thisRef: Preferences, property: KProperty<*>): String {
        val file = File(fileName)
        return if (file.exists()) file.readText() else ""
    }

    override fun setValue(thisRef: Preferences, property: KProperty<*>, value: String) {
        File(fileName).writeText(value)
    }

}

Now we can access the content of the file username.txt by accessing the property! Isn't that cool?

fun main() {
    val preferences = Preferences()
    println(preferences.username)
    preferences.username = "Marc"
    println(preferences.username)
}

Even better, we now can reuse the same functionality with multiple properties:

class Preferences {
    var username: String by FileDelegate("username.txt")
    var accessToken: String by FileDelegate("accessToken.txt")
    var favoriteFood: String by FileDelegate("favoriteFood.txt")
}

Wouldn't it be nice if we could even generate the filename according to the name of the property? Easy as pie, because each KProperty has a property.name we can use:

class FileDelegate : ReadWriteProperty<Preferences, String> {

    override fun getValue(thisRef: Preferences, property: KProperty<*>): String {
        val file = File(property.name + ".txt")
        return if (file.exists()) file.readText() else ""
    }

    override fun setValue(thisRef: Preferences, property: KProperty<*>, value: String) {
        File(property.name + ".txt").writeText(value)
    }

}

While only a small change, this makes the code even more readable. Of course, this comes at a cost we should be aware of: If someone refactors a property name, the filename will change, too. If we are willing to accept this, the code will read like this:

class Preferences {
    var username: String by FileDelegate()
    var accessToken: String by FileDelegate()
    var favoriteFood: String by FileDelegate()
}

And in addition, because we extend from ReadWriteProperty<Preferences, String>, we can let type inference automatically get the String return type:

class Preferences {
    var username by FileDelegate()
    var accessToken by FileDelegate()
    var favoriteFood by FileDelegate()
}

Let's say that favoriteFood.txt is generated by a different program, and we only want to read it here. We can just change var to val, which effectively hides the setter:

   val favoriteFood by FileDelegate()

I think this can come in pretty handy, and delegated properties can make your Kotlin projects nicer to read. Some things you could use delegated properties for:

  • Reading/writing things from disk
  • Caching values as well (instead of reading them again every time)
  • Reading/writing shared preferences in Android (see this great blog post on how to do that)
  • Getting something from the network (although you'll have to call runBlocking if you use coroutines, as this mechanism currently does not support suspending coroutines - at least not that I know of)
  • Do logging

Wait, there's more!

The Kotlin documentation on delegated properties contains more knowledge. For example, I already knew about the lazy delegate from Kotlin's stdlib, which can be used to get and store the result of some expensive operation:

val lazyValue: Int by lazy {
    calculateAnswer() // takes 7.5 million years to print 42
}

What I didn't know was that you can define the thread safety mode (as lazy is synchronized by default, which can add more overhead than you would expect):

val lazyValue: Int by lazy(mode = LazyThreadSafetyMode.NONE) {
    calculateAnswer() // takes 7.5 million years to print 42
}

Another useful thing I didn't know yet was that you can delegate to a map, like so for an immutable map as stated by the Kotlin docs:

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

Have an awesome Kotlin! πŸŽ‰

If you liked this post, please give it a ❀️, click the +FOLLOW button below and follow me on Twitter! This will help me stay motivated to create many more of these posts! 😊

Cover photo by Anton Sulsky on Unsplash.

Top comments (3)

Collapse
 
ansman profile image
Nicklas Ansman Giertz • Edited on

Delegates are very useful but making disk I/O or network calls using delegates are highly discouraged. Instead use an scope.async { loadStuff() } and read it from a suspending function if you want to cache the result. The kotlin style guide explicitly discourages you from making most of your examples delegates or properties:
kotlinlang.org/docs/reference/codi...

Lastly delegates are fairly expensive (they need to create a KProperty object for each property) and on performance sensitive platforms (such as Android) they should be used with care.

Great article though, they are a powerful tool that every Kotlin developer should have in their arsenal

Collapse
 
terkwood profile image
Felix Terkhorn

Thanks for writing this up! I appreciate the knowledge share.

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.