Kotlin Scope Function

Deepak Sikka
7 min readAug 15, 2021

This Story is from the Kotlin- Series.This article explain you about kotlin scope function & common Usage.

One of the main reasons developers are attracted to kotlin is how concise it is to write. Scope functions are one of the ways to write such concise code. According to the official documentation:

The standard scope functions that are provided by the language are of 5 types let, run, apply, also, with. Scope functions execute a block of code for a particular object within its context using a lambda expression.

Context object :
Each scope function uses one of the two ways to access the context object : this & it. this is called lambda receiver & it is called lambda argument. We will see in more detailed way about its usage.

(Let,also------> it)      (run,apply,with----> this).

‘this’ and ‘it’

Inside a lambda of a scope function, this (lambda receiver) is used to access the context object. The same object can be accessed by using it (lambda argument). In general, both can perform similar things.

this : run, apply, with use ‘this’ to access the context object.Having the context object as a receiver ‘this’ is recommended for lambdas that mainly operate on the object members: call its functions or assign properties.

it:let and also use ‘it’ to access the context object. Instead of ‘it’, we can also provide custom name.Having the context object as ‘it’ is better when the object is mostly used as an argument in function calls. ‘it’ is also better if you use multiple variables in the code block.

Result

The behaviour of scope functions can also be differentiated on the basis of their return values. There are two things to remember:

  1. apply and also return the context object.
  2. let, run, and with return the lambda result.

Now, I will try to explain the most common practical usages of the functions.

let

When should we use let function? Well, there are several scenarios. Most common scenario is null check. How did we usually write null checks?

Use let whenever you want to define a variable for a specific scope of your code but not beyond.

fun main() {
val obj = UserDetail("Deepak", 9999900111)

/* Normal null check */
if (obj != null) {
println(obj.userName)
}

/* Using let */
obj?.let {
println(it.userName)
}
}

We will take the example of webview.

//setting variable is not valid here. we can't use outside let block
webView.settings.let { setting ->
setting.javaScriptEnabled = true
setting.domStorageEnabled = true
setting.userAgentString = “mobile_app_webview”
}

In above example setting variable cant’ be used outside let block.So the problem of variable leak out will not happen here.This can also be used as an alternative to testing against null: like if webView.settings is null then this block will not execute.let is useful for checking Nullable properties.

We can chain multiple let functions.

val persons = mutableListOf(
UserDetail("Vinay", 9897878234),
UserDetail("Manish", 9896779157),
UserDetail("Kunal", 24989797979)
)
persons.filter { it.mobileNo > 11 }.map { it.userName }.let {
println(it)
}

Note: This is very simple example of chaining for easier understanding. It is more helpful in real life where there is bit of complexity in chaining.

run : run is actually a combination of with() and let()

run is useful when we initialise an object and perform some operation on that object. All this is done within a single run block. re-scopes the variable it’s used on to. The last expression returns a result.

run is also used for null check same as let.It is same as with call multiple different methods on the same object.

// run use case
val lengthOfName = UserDetail("Vinay", 9896779159).run {
println("mobile is $mobileNo")
println("Name is $userName")
}
print(lengthOfName)

We can chain multiple run functions.

//run chain
webView.settings.run {
javaScriptEnabled = true
domStorageEnabled = true
userAgentString = "mobile_app_webview"
webView //this will be return type of run
}.run {
webViewClient = MyWebViewClient()
loadUrl(mUrl)
}

run vs let

The difference is run refers to the context of the object as “this” and not “it”. One point here is that since the context is referred to as “this”, it cannot be renamed to a readable lambda parameter. So depending on the use case and requirement we have to choose between the let and the run operator.

With

with is not an extension function. The context object is passed as an argument and it is available as a receiver (this). The return value is the lambda result. It is similar to let with the difference that this is used as context object instead of it.

In the code, with can be read as “with this object, do the following.”

fun coolFunction() {
val person = UserDetail("Vinay", 29)
with(person) {
userName = "Sr. Developer"
mobileNo = 90
}
println(person)
}

with is not suited for null checking or call chaining

Let vs. with vs. run :

If we look at with and T.run, both functions are pretty similar. The below does the same thing.

with(webview.settings) {
javaScriptEnabled = true
databaseEnabled = true
}
// similarly
webview.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}

However, their difference is one is a normal function i.e. with, while the other is an extension function i.e. T.run.So the question is, what is the advantage of each?

Imagine if webview.settings could be null, they will look as below.

// Yack!
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}

In this case, T.run extension function is better, as we could apply to check for nullability before using it.

Now,we look at T.run and T.let, both functions are similar except for one thing, the way they accept the argument. The below shows the same logic for both functions.

stringVariable?.run {
println("The length of this String is $length")
}
// Similarly.
stringVariable?.let {
println("The length of this String is ${it.length}")
}

However, for T.let function signature, you’ll notice that T.let is sending itself into the function i.e. block: (T). Hence this is like a lambda argument sent it. It could be referred to within the scope function as it. So I call this as sending in it as an argument.

stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}

also

Common use of also is for side effects without modifying the object. We can use it for doing some operation on intermediate result. also does not transform the object. It returns same object.In Simple words,it returns the original object which means the return data has always the same type

T.also returns the T itself i.e. this
T.also let you perform on the same variable i.e. this
.

As per the official document :

When you see also in the code, you can read it as “and also do the following with the object.”

// also use case
val persons = mutableListOf(
UserDetail("Vinay", 29),
UserDetail("Manish", 30),
UserDetail("Kunal", 24)
)
val filteredResult = persons
.map { it.userName }
.also { println(it) }
.filter { it.length > 5 }
println(filteredResult)

let vs also :

stringVariable?.let {
println("The length of this String is ${it.length}")
}
// Exactly the same as below
stringVariable?.also {
println("The length of this String is ${it.length}")
}

However their subtle difference is what they return. The T.let returns a different type of value, while T.also returns the T itself i.e. this.Both are useful for chaining function, wherebyT.let let you evolve the operation, and T.also let you perform on the same variable i.e. this.

val original = "abc"
// Evolve the value and send to the next chain
original.let {
println("The original String is $it") // "abc"
it.reversed() // evolve it as parameter to send to next let
}.let {
println("The reverse String is $it") // "cba"
it.length // can be evolve to other type
}.let {
println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
println("The original String is $it") // "abc"
it.reversed() // even if we evolve it, it is useless
}.also {
println("The reverse String is ${it}") // "abc"
it.length // even if we evolve it, it is useless
}.also {
println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain
original.also {
println("The original String is $it") // "abc"
}.also {
println("The reverse String is ${it.reversed()}") // "cba"
}.also {
println("The length of the String is ${it.length}") // 3
}

When both combine the chain, i.e. one evolve itself, one retain itself, it becomes something powerful e.g. below

// Normal approach
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

apply

The context object is available as receiver (this). The return value is the object itself. The apply block is mainly used when we don’t worry about returning a value and instead mainly operate on members of the receiver object. We can use apply for the simplification of the complex chains of blocks.

Common use case of apply is object configuration. Below code comparison is self explanatory.

val dialog = CustomDialogFragment(this)
dialog.apply {
setCanceledOnTouchOutside(false)
setMessage("Do you want to cancel this transaction?")
setPositiveButton("Yes" )
setNegativeButton("No" )
}

Function selections

Hence clearly, with the 3 attributes, we could now categorize the functions accordingly. And based on that, we could form a decision tree below that could help decide what function we want to use pending on what we need.

Hopefully the decision tree above clarifies the functions clearer, and also simplifies your decision making, enable you to master these functions usage appropriately

Resources :

Thanks for reading.

--

--

Deepak Sikka

Senior Android Developer. Working on technology Like Java,Kotlin, JavaScript.Exploring Block Chain technology in simple words.