Kotlin’s standard library includes some often-used scope functions that are so abstract that even those who have been programming in Kotlin for a while can have a hard time keeping them straight. In this guide, we’re going to clarify four of these scope functions in particular - let()
, also()
, run()
, and apply()
. By the end of this guide, you’ll have a framework for understanding them and should have a good idea of which one is most applicable in different scenarios.
Key Characteristics
As a starting point for examination, let’s look at some code. Here’s an example of apply()
:
val date = Calendar.getInstance().apply {
set(Calendar.YEAR, 2016)
set(Calendar.MONTH, Calendar.FEBRUARY)
set(Calendar.DAY_OF_MONTH, 15)
}
In this code listing, you’ll notice a few things:
apply()
is an extension function -Calendar
doesn’t have anapply()
method. Kotlin extends it with this function inStandard.kt
.apply()
takes a block of code. Don’t be fooled by the lack of parentheses. When the only argument is a function, you can skip the parentheses and just use the curly braces.- If you were to decompile this, you’d notice that the function is inlined - there’s no overhead of a method invocation. Nice!
These three characteristics actually apply to all four functions, not just apply()
.
Distinguishing Characteristics
So how do they differ? They differ on two dimensions:
- The context of the code block
- The value that is returned by the function
Context of the Code Block
The first dimension is the context of the code block.
Extension functions have a receiver - that is, an object upon which that extension function is invoked. In our example above, the receiver is an instance of Calendar
- the instance that was returned by getInstance()
.
Inside the code block, there are two options for the context:
- The context can be preserved. In this case, the value of
this
does not change, but the receiver object is accessible as the first lambda argument, which by default isit
. - The context can become the receiver object. In other words, inside the code block,
this
can refer to the receiver.
In our code listing above, you’ll notice the three calls to set()
are setting values on the Calendar
instance. So apply()
falls under the second case - this
refers to the Calendar
instance.
Let’s see the same example, but this time using also()
:
val date = Calendar.getInstance().also {
it.set(Calendar.YEAR, 2016)
it.set(Calendar.MONTH, Calendar.FEBRUARY)
it.set(Calendar.DAY_OF_MONTH, 15)
}
This time, since we’re using also()
instead of apply()
, we had to prefix the set()
calls with it
. This is because this
still refers to the same thing as it did outside of our code block.
Return Value
The second dimension is the return value.
The result returned by the function can be one of two things:
- The receiver object itself.
- Whatever the code block returns.
Here’s the apply()
example again, but this time with the type of date
indicated explicitly:
val date: Calendar = Calendar.getInstance().apply {
set(Calendar.YEAR, 2016)
set(Calendar.MONTH, Calendar.FEBRUARY)
set(Calendar.DAY_OF_MONTH, 15)
}
As you can see, the result of calling apply()
is a Calendar
- it’s the same instance as the receiver object that we called apply()
on. So apply()
fits in the first category - it returns the receiver object.
Now instead of needing the full date, let’s say we just need to know the day of the year. Here’s how we can update that last code listing to do that.
val date: Int = Calendar.getInstance().apply {
set(Calendar.YEAR, 2016)
set(Calendar.MONTH, Calendar.FEBRUARY)
set(Calendar.DAY_OF_MONTH, 15)
}.get(Calendar.DAY_OF_YEAR)
But with run()
, the result returned is the result of the code block, so we’d shuffle that call to get()
:
val date: Int = Calendar.getInstance().run {
set(Calendar.YEAR, 2016)
set(Calendar.MONTH, Calendar.FEBRUARY)
set(Calendar.DAY_OF_MONTH, 15)
get(Calendar.DAY_OF_YEAR)
}
Keeping them Straight
These two dimensions - context and result - can form a nice little grid that can make it easy to remember what’s what:
Return Block Result | Return Receiver | |
---|---|---|
Context Unchanged - it |
let() |
also() |
Context Changed - this |
run() |
apply() |
Following the first letter of each, you get the acronym “LARA”.
Code Comparison
The following demonstrates the four functions, accomplishing the same thing in each case - printing Hello
and then returning the length of the string.
|
|
|
|
Choosing from among the LARA functions
Do you want to preserve the this
scope?
Your code block might need to reference the existing this
scope. In this case, you would probably prefer the top row - either let()
or also()
. Of course, you can reference the this
of outer scopes, but it’s a bit more verbose.
On the other hand, if your code block is calling lots of functions on the receiver, you get better signal-to-noise ratio by using one of the two functions from the bottom row - either run()
or apply()
, because you won’t need to specify the object at all.
Compare the two EditText
objects being created in this code listing.
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_example)
// The calls to the setters are a tad more verbose
// The call to `getString()` is concise
val view = EditText(this).also {
it.setText(getString(R.string.app_name))
it.visibility = View.VISIBLE
it.setSelection(0, 4)
}
// The calls to the setters are more concise
// The call to `getString()` is more verbose
val view2 = EditText(this).apply {
setText(this@ExampleActivity.getString(R.string.app_name))
visibility = View.VISIBLE
setSelection(0, 4)
}
}
}
Are you just initializing an object?
If so, you’ll want one of the functions on the right-hand column, because they return the receiver object when you’re done.
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_example)
// Concise - receiver object is returned
val view = EditText(this).also {
it.setText(getString(R.string.app_name))
it.visibility = View.VISIBLE
it.setSelection(0, 4)
}
// Verbose - gotta add `it` at the end to return it
val view2 = EditText(this).let {
it.setText(getString(R.string.app_name))
it.visibility = View.VISIBLE
it.setSelection(0, 4)
it // <-- verbosity here
}
container.addView(view)
}
}
Common use cases for each
Here are some of the more common use cases:
- Transforming an object -
let()
- Create, pass, and evaluate -
also()
- Initialize and execute -
run()
- Initialize an object for assignment -
apply()
If you’re looking for more information about each of these four functions, check out their respective pages in the Concepts section.