Learning Kotlin: The Lazy Delegate

Note This is the 26th post in a multipart series. If you want to read more, see our series index. The koans used here are 34 and 35.

Following on from our introduction to the by operator and delegates, this post looks at the first of the five built-in delegates, lazy. Lazy property evaluation allows you to set the initial value of a property the first time you try to get it. This is really useful for scenarios where there’s a high cost of getting the data. For example, if you want to set the value of a username that requires a call to a microservice—but since you don’t always need the username—you can use this to initialize it when you try to retrieve it the first time.

Setting up the context for the example, let’s look at two ways you might do this now:

  1. First way: You call the service in the constructor, so you take the performance hit immediately—regardless of whether you ever need it. It’s nice and clean though.

    class User(val id: Int) {
        val name: String
        constructor(id: Int) {
            // load service ...
            // super expensive
            Thread.sleep(1000)
            name = "Robert"
        }
    }
    
    fun main(args: Array<String>) {
        val user = User(1)
        println(user.name)
    }
    
  2. Second way: You add a load function, so you need to call that to load the data. This avoids the performance hit but is less clean—and if you forget to call load, your code breaks. You might think, “That will never happen to me…” I just did that right now while writing the sample code. It took me less than 2 minutes to forget I needed it. 🙈 Another pain: I need to make my name property mutable (var) since it’ll be assigned later.

    class User(val id: Int) {
        var name: String = ""
        fun load() {
            // load service ...
            // super expensive
            Thread.sleep(1000)
            name = "Robert"
        }
    }
    
    fun main(args: Array<String>) {
        val user = User(1)
        user.load()
        println(user.name)
    }
    

The solution to this is obviously lazy—it gives us the best of all of the above (clean code, no performance hit unless we need it, can use val, and no forgotten calls) with none of the downsides.

class User(val id: Int) {
    val name: String by lazy {
        // load service ...
        // super expensive
        Thread.sleep(1000)
        "Robert"
    }
}

fun main(args: Array<String>) {
    val user = User(1)
    println(user.name)
}