Scope Functions in Kotlin
Kotlin has many language benefits over Java, which is often criticized for being too verbose. On Android, you’re stuck with using Java 8, so all the improvements to Java that have come since aren’t available to you. Hopefully you’ve heard of Kotlin by now, and hopefully given it a try. It has 100% interoperability with Java since it compiles down to the same byte code, so you can easily use both languages within the same project. Kotlin has many wonderful features that make Android programming much nicer – coroutines, extension functions, higher order functions, typealiases – the list goes on. But today I wanted to talk about Scope Functions
. You might have seen these before and wondered what the difference between them is.
Here’s a list:
let
also
with
run
apply
So what’s the difference between these? When should you use them?
Let
Let’s start with let, since this one is potentially the easiest to understand. Imagine you have an Optional value – String, Int, whatever. What let
does is, when used on an optional value, the lambda’s context object will be the safely unwrapped value. That’s to say, if the value of the optional is null, the lambda won’t be executed.
var optionalString: String? = null
optionalString?.let { it.length }
The context object passed to the lambda – it
– is the value of optionalString
if it has one. The result of using let
will be the lambda result.
Run
Run is similar to let, with the main difference being that the context object passed to the lambda is available as a receiver – that is to say, within the lambda, this
is whatever object you’ve executed run
on. If that’s a bit confusing, don’t worry – it is. But here’s a little example which should hopefully clear some questions.
var myObject = MyObject()
myObject.let { it.doSomething() }
myObject.run { doSomething() }
Notice how with run I haven’t had to use it
to call doSomething()
. That’s because the context this
is MyObject
. The benefit of this is that if you only care about MyObject
and you’re calling a bunch of functions on it within the lambda, you don’t have to call it
or give the passed in value a name. The issue with this is if you want to use some other context within your lambda, you lose the ability to reference that. You’ll probably run into this most commonly when you’re in an Activity or Fragment and want to get context
, within a run
lambda you don’t have the correct context to get it, but within a let
lambda you do.
Another great thing about using run
is as a non-extension function. So for example, imagine you want to create a variable that isn’t just a single line assignment, but instead you want the value of the variable to be the result of a series of expressions. You could use by lazy
, but this means the expressions will only be executed the first time the variable is accessed. This might be what you want, but it also might not be. Using run
allows you to do this in a non-lazy way.
val calculation: Calculation = run {
val firstNumber = 4 * 5
val secondNumber = 8 4
Calculation(firstNumber, secondNumber)
}
Now, when the class that holds this variable is initialized, the run block will be executed and the result will be assigned.
With
with
is slightly different from run
and let
. with
can’t be used as an extension on an object, and so instead you use it like using any other function. You pass it an object, and then within the lambda, the context is the object you passed in. Like run
and let
, with
returns the lambda result. As an example, let’s say you have a WebView subclass, and within the initializer, you want to set a bunch of things on the settings
object. You could do this:
settings.setJavaScriptEnabled(true)
settings.setDomStorageEnabled(true)
settings.setWebViewClient(webViewClient)
But when using with
, this can be simplfied to the following:
with(settings) {
setJavaScriptEnabled(true)
setDomStorageEnabled(true)
setWebViewClient(webViewClient)
}
When you’re setting a lot of things, or doing a lot of things with an object, this can make the code much easier to read, in addition to not having to write settings.
before everything.
Apply
apply
is an extension function which passes the object it was called on as the context to the lambda. The return value of using apply
is the object itself, not the result of the lambda. It’s easiest to demonstrate this with a small code example.
val myFragment: MyFragment = MyFragment()
myFragment.something = true
myFragment.anotherThing = “eggs”
myFragment.somethingElse = 50
This can be rewritten to the following:
val myFragment: MyFragment = MyFragment().apply {
something = true
anotherThings = “eggs”
somethingElse = 50
}
Also
The final scope function is also
. also
can be thought of as a way to perform a side effect, rather than modifying an object like the scope functions. So for example, let’s say you want to log something before you change it and then immediately after you’ve changed it. The traditional Java approach would be something like this
val numbers = mutableListOf(“one”, “two”)
Log.d(TAG, numbers.size.toString())
numbers.add(“three”)
Log.d(TAG, numbers.size.toString())
With also
, this can be rewritten as:
val numbers = mutableListOf(“one”, “two”)
numbers
.also { Log.d(TAG, it.size.toString() }
.add(“three”)
.also { Log.d(TAG, it.size.toString() }
It’s a small change in this example, but can be used to make code a lot more readable. You could, for example, make it so that any time a variable is accessed, an also
block is executed that logs its value, for example.
Hopefully this article helps clear up any confusion you have about the various scope functions and what each of them do. In addition to this, the official Kotlin docs has a very useful table that breaks down these scope functions, just in case you briefly forget what a particular one does.