Scoped Functions

Scoped Functions in Kotlin

By definition, Scoped functions are functions that execute a block of code within the context of an object. (apply, also, run, let, with)


The context of the object can be referred to as “it” or “this” 

Differentiating scoped functions with examples

There are five scoped functions in Kotlin: let, run, with, also and apply. Let’s go through them one by one.

let’s consider a Model class “Person”

class Person() {
var name: String = "Abcd"
var contactNumber: String = "1234567890"
var address: String = "xyz"
fun displayInfo() = print("\n Name: $name\n " +
"Contact Number: $contactNumber\n " +
"Address: $address")

}


Let:

Let’s consider the following function:

private fun performLetOperation() {
val person = Person().let {
"The name of the Person is: ${it.name}"
}
print(person)
}
output:
The name of the Person is: Abcd

From the above code snippet, we can see that although “let” operation is performed on a Person object, the output of the code is a string value and not the Person object. This implies that the “let” operator provides an option to perform an operation on the current object and return any value based on the use case.

..

It refers to the context of the object by using the “it” keyword and hence, this “it” can be renamed to a readable lambda parameter.

private fun performLetOperation() {
val person = Person().let { personDetails ->
personDetails.name = "NewName"
}
print(person)
}

It easily helps in providing null checks. Let’s say we make the “name” parameter of the “Person” class nullable and we want to print the name of the person only if it is a not null value, then we can write a clean, simple and concise code as follows:

apple

“let” can also be used when we want to perform an operation on the result of a call chain. Let’s take the following example:

fun main() {
val numbers = mutableListOf("One","Two","Three","Four","Five")
numbers.map { it.length }.filter { it > 3 }.let {
print(it)
}
}

let scope function is used to apply operations on an object and finally return the lambda expression from that scope function. 

The return type can also be void.

Webview eg:

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

//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"
}

This can also be used as an alternative to testing against null: if webView.settings are null then this block will not execute.

let is useful for checking Nullable properties.

We can chain multiple let functions.

//Let chain 
//setting is variable name defined by us. By defualt it is it
webView.settings.let {setting ->
setting.javaScriptEnabled = true
setting.domStorageEnabled = true
setting.userAgentString = "mobile_app_webview"
webView //this will be return type of let block
}.let {
it.webViewClient = MyWebViewClient()
it.loadUrl(mUrl)
}


run

Run is actually a combination of with() and let().

  • It is the same as let if webView.settings is null then this block will not execute
  • It is the same as with call multiple different methods on the same object

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

So if run is similar to let in terms of accepting any return value, what’s the difference? The difference is that run refers to the context of the object as “this” and not “it”. That is the reason we did not use “${this. name}” as it would be redundant here since the block of code understands that “name” is used here concerning the Person object.

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. The “run” operator also helps in easy null checks similar to the “let” operator

var name: String? = "Abcd"
private fun performRunOperation() {
val name = Person().name?.run {
"The name of the Person is: $this"
}
print(name)
}
Output:
The name of the Person is: Abcd


with

It is convenient when you have to call multiple different methods on the same object. Instead of repeating the variable containing this object on each line, you can use with.

with is used to change instance properties without the need to call dot operator over the reference every time.

  • This is a normal function.
  • It is not an extension function.
  • last expression of with function returns a result.
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.userAgentString = "mobile_app_webview"

with(webView.settings){
javaScriptEnabled = true
domStorageEnabled = true
userAgentString = "mobile_app_webview"
webview // this is last statement, so it will be return type of with
}
..

with vs run

So, if “with” is the same as “run”, what’s the difference? How should we choose between these two? Now, this is an interesting case. Let’s consider a case where a Person object can be nullable.

we can see that the context of the object referred to as “this” is a nullable type of Person. 

private fun performWithOperation() {
val person: Person? = null
with(person) {
this?.name = "asdf"
this?.contactNumber = "1234"
this?.address = "wasd"
this?.displayInfo()
}
}

So performing a null check using a “with” operator is difficult and this is where we can replace it with “run” as follows:

private fun performRunOperation() {
val person: Person? = null
person?.run {
name = "asdf"
contactNumber = "1234"
address = "wasd"
displayInfo()
}
}


apply

It is an extension function. It runs on the object reference (also known as a receiver) into the expression and returns the object reference on completion.

  • In apply it isn’t allowed.
  • It sends this as it’s an argument.
  • It returns this (i.e. itself)

apply in combination with let
webView.settings.apply { 
setting-> // this is invalid as it is not allowed
javaScriptEnabled = true
domStorageEnabled = true
userAgentString = “mobile_app_webview”
}

//This is valid
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
userAgentString = “mobile_app_webview”
}.let {
webView
}
.apply {
webViewClient = MyWebViewClient()
loadUrl(mUrl)
}
apply in combination with run
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
userAgentString = “mobile_app_webview”
}.run {
webView //this is return type of run
}
.apply {
webViewClient = MyWebViewClient()
loadUrl(mUrl)
}

The apply function is similar to the run functionality only in terms of referring to the context of the object as “this” and not “it” and also in providing null safety checks:

 run accepts a return statement whereas “apply” does not accept a return statement

private fun performApplyOperation() {
val person: Person? = null
person?.apply {
name = "asdf"
contactNumber = "1234"
address = "wasd"
displayInfo()
}
}

..

"apply" use-case in Android Development

Specifically for Android Development, "apply" can be useful in many cases. We have many scenarios where we should return an instance of Intent or an Alert Dialog etc., by adding specific attributes to them.

..


also

The “also” function is similar to the let functionality only in terms of referring to the context of the object as “it” and not “this” and also in providing null safety checks:

The advantage of using “also” operator is that while doing a chain of operations

private fun performAlsoOperation() {
val name = Person().also {
print("Current name is: ${it.name}\n")
it.name = "ModifiedName"
}.run {
"Modified name is: $name\n"
}
print(name)
}

This way we can execute the intermediate result within the execution chain without breaking the chain, thereby leading to a better readable code.

 let accepts a return statement whereas “also” does not accept a return statement

..

also

  • It passes an object as a parameter and returns the same object.
  • It returns the original object which means the return data has always the same type

also in combination with let

//setting is variable name defined by us. Bydefualt it is it
// also with let
webView.settings.also { setting ->
setting.javaScriptEnabled = true
setting.domStorageEnabled = true
setting.userAgentString = “mobile_app_webview”
}.let {
webView //this is return type of let
}.also {
it.webViewClient = MyWebViewClient()
it.loadUrl(mUrl)
}

also in combination with run

//setting is variable name defined by us. Bydefualt it is it
//also with run cobination
webView.settings.also { setting ->
setting.javaScriptEnabled = true
setting.domStorageEnabled = true
setting.userAgentString = “mobile_app_webview”
}.run {webView}
.also {
it.webViewClient = MyWebViewClient()
it.loadUrl(mUrl)
}

..

Comments