Semantical let, apply, also, run & with
Kotlin defines functions named let
, apply
, also
, run
, and with
that can help us write cleaner, more focused code by creating temporary scopes.
Hence the name ‘scope functions’.
The scope functions do not introduce any new technical capabilities, but they can make your code more concise and readable.
I find it unsatisfactory to describe their differences just by focusing on their signatures, what do they return, and whether the receiver is accessed via it
or this
, like in the following examples.
|
|
|
|
Official Kotlin documentation explains the technical aspect well and also presents code samples. I don’t want to delve into details about how they are defined but, instead, I would like to suggest a more semantical approach to choosing one over another.
Ultimately, any of them can, with minor differences, achieve the same goal. But choosing the right function may better convey your intention—and as such, make the code easier to read and understand.
When to use one
All of the scope functions (with an exception of with
) work nicely in combination with ?.
and/or mutable receivers.
val content: View? = findViewById<View>(R.id.contennt)
if (content != null) {
// do something with content
}
findViewById<View>(R.id.content)?.let { content: View ->
// do something with content
}
The first obvious effect is that we don’t have to explicitly check for the nullability of content
.
By using ?.
the scope function will execute only if the receiver is not null and non-null value is provided inside the function.
The other less obvious effect is that content
is only available inside the let
scope.
This is desirable as you should always strive towards limiting the scope of variables and functions.
You would not declare a variable in a global scope if it was used only by a single class, would you?
Instead, you would just declare it in the class where it is used.
Likewise, you would not declare a variable in the class scope if it was only used locally in a single function.
Similar reasoning can be applied to variables inside functions as well.
Often a variable’s sole purpose is to calculate the value of another variable.
In the first example, the scope of content
leaks to all further, potentially dozens of expressions below the if
statement.
By using a scope function we can eliminate the variable and make it dead obvious it is not used again later in the function.
Which one to use
let
Often used in place of if (value != null)
to conditionally execute a piece of code
when there is not much difference between using let
and other scope functions.
Because it is used so commonly in many situations, using let
does not provide as many semantical clues as other scope functions.
iconView?.let {
when (intent.getStringExtra(EXTRA_ACTION)) {
EXTRA_ACTION_CONCEAL -> conceal(it)
EXTRA_ACTION_REVEAL -> reveal(it)
}
}
You can think of it as a map
function being used on a single value.
fun stringToInstant(time: Long?): Instant? {
return time?.let { Instant.ofEpochMilli(it) }
}
val fragment: Fragment = intent.extras?.getNullableLong(ARG_NOTE_ID).let { noteId ->
if (noteId == null) {
NoteDetailFragment.newAddFragment()
} else {
NoteDetailFragment.newEditFragment(noteId)
}
}
also
Commonly used to perform additional operations with the receiver object without the necessity of storing it in a variable.
return DocumentFile.fromTreeUri(context, uri)?.also {
loadFolder(it)
}
You can think of it as an onEach
function being used on a single value.
getSongsInPlaylist()
.also(::println)
.groupingBy(Song::author)
.eachCount()
apply
Use apply
when your primary intention is to apply multiple changes to the receiver.
Use it primarily for mutable operations, i.e., in situations when the receiver would be on the left side of assignments or calling mutable methods on the receiver.
continueButton = findViewById<Button>(R.id.continue_button).apply {
text = buttonText
setOnClickListener {
onContinueClicked()
}
}
(parentFragment as? WordUpdatedListener
?: activity as? WordUpdatedListener)
?.apply {
if (wordId == null) {
onWordAdded(text)
} else {
onWordUpdated(wordId, text)
}
}
You can think of it as an alternative to a builder pattern that returns the same object after the invocation of each method.
TransitionSet().apply {
interpolator = LinearInterpolator()
duration = 500
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(Fade(Fade.OUT))
addTransition(ChangeBounds())
addTransition(Fade(Fade.IN))
}
return SongsListFragment().apply {
arguments = Bundle().apply {
putString(KEY_URL, url)
putString(KEY_EDITABLE, false)
}
}
run & with
Use run
or with
when you want to have implicit access to members of the receiver but don’t intend to modify it.
Use them primarily for immutable operations, i.e., in situations when the receiver would be on the right side of assignments or calling immutable methods on the receiver.
with(viewModel) {
messagesLiveData.observe(viewLifecycleOwner, Observer {
adapter.messages = it
})
continueEnabledLiveData.observe(viewLifecycleOwner, Observer {
continueButton.isEnabled = it
})
}
val confirmationMessage: CharSequence = with(context.resources) {
buildSpannedString {
append(getString(R.string.confirm_delete_songs_prefix))
bold {
append(getQuantityString(R.plurals.songs, count, count))
}
append(getString(R.string.confirm_delete_songs_suffix))
}
}
with
cannot be used in conjunction with .?
to execute only when the receiver is non-null. In such cases use run
.
arguments?.run {
viewModel.loadWordDetail(getLong(ARG_WORD_ID))
}
Use run
when the receiver object is a complex expression and would be difficult to read using with
.
Explicit it vs implicit this
run
/with
and apply
functions override what object this
refers to.
When it is not immediately obvious whose members are being accessed inside the scope of run
/with
or apply
, consider replacing them with let
or also
.
return createView(AnkoContext.create(context)).apply {
// Is `getTag()` called on the created view or the outer class?
// Do we assign the value to a property of the created view or the outer class?
layout = getTag() as View
}
Likewise, consider using let
or also
when the scope of run
/with
or apply
would shadow members of the outer scope.
class DocumentSelectionFragment : Fragment() {
private var documentTypeSelectedListener: DocumentTypeSelectedListener? = null
fun updateExtras(documentExtras: DocumentExtras?) {
// Use `let` to avoid unnecessary or excessively long @labels.
documentExtras?.run {
this@DocumentSelectionFragment.documentTypeSelectedListener = documentTypeSelectedListener
}
}
}
Beware of ?: opeartor
As the last point, I would like to warn about a common trend of using ?:
as a replacement for an if
statement.
user?.let {
process(it)
} ?: processDefault()
One would expect that if the user
is null, process
will be executed and processDefault
otherwise.
In most cases, that assumption holds but there is a problem with the let
function.
Since the return value of let
is the last expression of the block, ?:
checks the nullability of the returned value, not the user
.
If it is not obvious yet, in the case process
function returned null
, both process
and processDefault
would be executed.
This actually happened in my previous workplace. The user
object sometimes got into an impossible state, causing us to scratch our heads for hours while searching for the bug.
As much as I love functional programming, I have since decided to avoid this approach and use plain if
statements, so that it does not confuse developers not aware of the problem.
Another solution would be to use also
which returns the same object as it was called on and does not suffer from the same problem as let
.
Conclusion
Hopefully, by now you can see the differences between scope functions not only from the technical perspective but also from the semantical perspective. Note that these are just personal suggestions on how to make the code a bit easier to understand by providing semantical hints to the reader and they are certainly not meant to be strictly followed in every situation.
Sometimes it can be difficult to decide which of the functions to use in a given situation.
In case you want to use the receiver for both mutable and immutable operations, should you use apply
or run
?
Or should you rather separate mutable operations from the immutable ones and use both apply
and run
?
It heavily depends on the situation. My advice would be not to fret about it too much and use whatever feels more appropriate to you.