Hoinzey

Javascript. Kotlin. Android. Java

Kotlin: Scope functions

I'm talking about scope functions. There are 5 of them and if you've spent any time in a Kotlin project you've definitely seen them: let, with, , run and also. They are also known as lambdas with receivers. I will only look at the first 3 as they are the most common.

They all allow you to execute code on an object but differ in how that object is treated within the scope of the block.

with

Takes the first argument to a receiver of the lambda that is the second argument. You can access the receiver explicitly using this. In most cases though, this is unnecessary.

The return value is the lambda result which is why you'll see the toString() call after the curly braces.


                    
    data class DeliveryAddress(
        var firstName: String,
        var addressLine1: String,
        var postCode: String,
        var mobileNumber: String) {
    
        var surname: String? = null
        var addressLine2: String? = null
        var addressLine3: String? = null
        var phoneNumber: String? = null
    
        override fun toString(): String {
            return with(StringBuilder()) {
                append("$firstName $surname,").appendLine()
                    .append(addressLine1).appendLine()
                    .append(addressLine2).appendLine()
                    .append(addressLine3).appendLine()
                    .append(postCode).appendLine()
                    .append(phoneNumber).appendLine()
                    .append(mobileNumber)
    
            }.toString()
        }
    }
                

apply

Apply works much the same except that it always returns the object passed to it. It works like an extension function on the object and is useful as a Builder of objects.


                    
    fun buildAddress(): DeliveryAddress {
        return  DeliveryAddress("Hoinzey",
            "line 1",
            "Posty",
            "148984019820").apply {
            surname = "Jones"
            addressLine2 = "SA"
            addressLine3 = "Lot 3"
            phoneNumber = "2222"
        }
    }
                

let

The return value is the result of the lambda. The context object is available in the lambda as it, I most commonly use it to execute blocks of code on non-null values


                    
    val map = mutableMapOf()
    //stuff happens 
    val result = map[i]?.let { 
    //do something
    }
                

Build your own

Below shows you the syntax differences between defining a standard lambda and one which has a receiver.


Standard lambda
Lambda with receiver
                    
    fun buildStringTakingLambda(builderAction: (StringBuilder) -> Unit): String {
        val sb = StringBuilder()
        builderAction(sb)
        return sb.toString()
    }

    fun printFromLambda() {
        println(buildStringTakingLambda {
            it.append("hello")
            it.append(" world")
        })
    }
                                        
                    
    fun buildStringLambdaWithReceiver(builderAction: StringBuilder.() -> Unit): String {
        val sb = StringBuilder()
        builderAction(sb)
        return sb.toString()
    }

    fun printFromLambdaWithReceiver() {
        println(buildStringLambdaWithReceiver {
            append("Receiving")
            append(" World")
        })
    }