Kotlin Scope Functions Made Simple

When to use them? Which one you should use? Or even something simpler, like what is the difference between them?

Scope Functions do not introduce new technical capabilities, nor do they have a real impact on performance.

So you may wonder, what´s the point in using them?

Well, they are here for the same reason that the Kotlin Language was intended for. Making code easier to read, more concise, and more efficient to write.

Key Concept: Context Object

I was quite unable to wrap my head around this concept, but trust me, once you get ahold of it, everything will start to make sense.

Scope functions allow you to create a temporary scope for an object. The way in which the object is referenced inside this new scope is as follows:

this

Inside a scope function, you will be able to reference the context object by a short word (this), instead of the name itself. So for example:

fun createCard () {

val card = Card("4421 9982 **** ****").apply {

cvv = "451" //equal to this.cvv = "451" or card.cvv = "451”

bank = "Huge Bank"

}

}

Inside the scope of .apply, whenever we refer to a variable of the Card object, we actually do not need to reference the Card object directly. We can access the variables cvv or bank directly.

Let´s see another example:

class MainActivity : AppCompatActivity() { lateinit var myIntent: Intent

val data = Uri.parse("anyString") fun foo(){

myIntent?.run {

data = this@MainActivity.data

startActivity(this)

}

}

In here, we need to access both the data variable from myIntent, and data variable from MainActivity. But we are already inside the scope of myIntent, so how can we access the variable data from MainActivity, the outer class?

Simple, by using the notation this@MainActivity.data.

Actually, in this last example, we are missing the whole point of using scope functions. They should make our code easier to read and understand, but this is making our lives much more complicated.

Solution: use it instead of this

In cases like this last one, where we need to access an object from outside the scope function, we can use the keyword it to reference the variables inside the scope function, like this:

fun foo(){

myIntent?.let {

it.data = data

startActivity(it)

}

}

Now that´s what I call readable and concise code =).

it now references myIntent, whilst this references the outer class, MainActivity.

So, enough of the introduction, now we are ready to talk about the different scope functions.

let

Referenced by -> it

Returns -> last statement

Use case -> Null checks

Mostly used for null checks, when applying ?.let on an object, we can rest safe that every time we access that object inside the scope function, the object will be not null. To reference the object inside the scope function, we use the keyword it.

var cardNumber: String? = "1233 1231"

fun printCard() {

cardNumber?.let {

// Everything executed inside this block will be null safe.

print("The length of the card number is ${it.length}")

}

}

apply

Referenced by -> this

Returns -> same object

Use case -> Initialize and configure an object

Basically, if you are initializing an object, and setting a bunch of properties like in this case, you have a pretty solid candidate to apply this scope function.

cardDrawer.visibility = View.VISIBLE

cardDrawer.setBehaviour(CardDrawerView.Behaviour.RESPONSIVE)

cardDrawer.show(config)

cardDrawer.setInternalPadding(0)

cardDrawer.setArrowEnabled(miniCard.showChevron)

The same behavior using apply:

cardDrawer.apply {

visibility = View.VISIBLE

setBehaviour(CardDrawerView.Behaviour.RESPONSIVE)

show(config)

setInternalPadding(0)

setArrowEnabled(miniCard.showChevron)

}

also

Referenced by -> it

Returns -> same object

Use case -> Additional actions that don´t alter the object, such as logging debug info.

cardDrawer.apply {

visibility = View.VISIBLE

setBehaviour(CardDrawerView.Behaviour.RESPONSIVE)

show(config)

setInternalPadding(0)

setArrowEnabled(miniCard.showChevron)

}.also {

Log.d("TAG", "Card drawer initialized with $it.behaviour")

}

Same example as before, but we also need to log additional info.

You may ask yourself, can´t we log the info inside the apply function? Well yes, you can, but we would be missing the whole point of using scope functions, improving readability.

The example could be read as: We use the apply function to initialize and configure an object, but we also need to log some additional info.

Good practice -> We should be able to remove the also scope function and not break any logic in our code.

run

Referenced by -> this

Returns -> last statement

It is the only scope function that has two variants.

run as extension -> used to create a scope to run an operation over an object, and get a result.

val message = StringBuilder()

val numberOfCharacters = message.run {

append("This is a transformation function.")

append("Any String")

length // number of characters takes the value of length

}

Note that run returns the last statement of the function. So it is useful when you and need to run certain operations over an object, and finally return one last operation, like the example.

2. run as function -> reduce the scope of certain variables, to avoid unnecessary access.

val isCardValid = run {

// Only visible inside the lambda

val cvv = getCvv()

val cardHolder = getCardholder()

validate(cvv, cardHolder)

}

In this case, we have decided to put the variables cvv and cardHolder inside the run function, making them invisible from outside the scope function.

with

Referenced by -> this

Returns -> last statement

Use case -> Run multiple operations on an object

val webview = WebView(this)

webview.settings.javaScriptEnabled = true

webview.loadUrl("https://www.mercadolibre.com")

When we use with:

val webview = WebView(this)

with (webview) {

settings.javaScriptEnabled = true

loadUrl("https://www.mercadolibre.com")

}

As you can see, it is very similar to apply. In fact, I rarely use with since it doesn´t allow me to do a null check, whilst ?.apply does.