Use Cases of Kotlin’s apply, also, let, with and run
There are so many articles to explain their differences and how to select one by each use case. Here I just simply share our team’s consensus on the usages of these functions.
TL;DR
- apply
we have the object T and want to apply more post-addition configuration to T itself. - also
we have the object T and also want to do something which been getting through T. - let
use let for transformations or null check when the context object T is treated as an Object in the block. - run
use run for transformations or null check when the context object T is treated as a Subject in the block. - with
we know the context object T is non-null, and we need to access directly.
In the first place, I would like to share these great articles which explained these functions, e.g
- Mastering Kotlin standard functions: run, with, let, also and apply
- The difference between Kotlin’s functions: ‘let’, ‘apply’, ‘with’, ‘run’ and ‘also’
- Subtle differences between Kotlin’s with(), apply(), let(), also(), and run()
- https://twitter.com/ppvi/status/1081168598813601793?s=21
These articles already explain these functions in detail, so I just want to share their use cases and the reason why our team prefers certain one function in that case. Generally, they are useful to replace the null checks, to execute blocks if this context object isn’t null, so here I will skip this kind of use case.
Let’s start from apply
and also
, both are useful and frequently used, especially during initialization. But they are slightly different, apply
is used to initialize the object itself, and also
is doing things through the object. In the opinion of our team, we tend towards semantics more than syntax.
- apply
<T> T.apply(block: T.() -> Unit): T
It’s useful when we have or create the object T and want to apply more post-addition configuration to T itself. Inside the block, we would focus on the set up of the context object. Since apply
returns the receiver in the end, it’s very convenient for the initialization:
view.findViewById<TextView>(R.id.info)?.apply {
text = “”
visibility = View.GONE
}author.animate().apply {
translationX(0f)
duration = 200L
interpolator = moveInterpolator
}
- also
<T> T.also(block: (T) -> Unit): T
This one is that we have an object T and also want to do something related and been getting through T, it is more convenient to complete together. In this case, also
is very similar to “by the way.”
For example, android Fragment inflates a root view at onCreateView(), and usually, we don’t only inflate the main view for our fragment but also get other subviews by findViewById(). But, by the way, we apply the default values to these subviews:
private lateinit var titleView: TextView
private lateinit var button: Button
private lateinit var recyclerView: RecyclerViewoverride fun onCreateView(inflater, container, savedInstanceState): View? {
return inflater.inflate(R.layout.fragment_store_profile, container, false)?.also {
titleView = it.findViewById<TextView>(R.id.title).apply {
text = "Welcome"
}button = it.findViewById<Button>(R.id.action).apply {
text = "Exit"
visibility = View.INVISIBLE
}recyclerView = view.findViewById<RecyclerView>(R.id.product_list).apply {
layoutManager = LinearLayoutManager(activity)
addItemDecoration(ProductItemDecoration())
}
}
}
And now let’s explore other functions, let
, with
and run
. All three functions return another value from blocks, which means they are suitable for transformations. In general, there is no particular distinction between let
and run
for transformations, but if the context object is treated as an Object in the block, use let
is more intuitional, otherwise; if it is treated as a Subject, we will prefer to use run
.
- let
<T, R> T.let(block: (T) -> R): R
It’s common to use let
for transformations, there is a good example from Elye, and I just do a little modification: this function uses let
to do null check and transform String to File, and then use also
to create the directory:
fun makeDir(path: String?) =
path?.let{ File(it) }.also{ it.mkdirs() } ?:
throw IllegalStateException("Invalid Dir Path!")
More often, we want to do some operations when we map a nullable type value, we will prefer to use let
when the context object T is treated as an Object in the block, for example,
person.email?.let { sendEmail(it) }
- with
<T,R> with(receiver: T, block: T.() -> R): R
It’s seldom seen in our project. We can use with
for transformation, but generally, we prefer let
or run
. However, when we know the context object T is non-null, and we need to access directly, it’s good to use with
, for example,
with(employee) {
println("name:$employeeName, title:$jobTitle")
}
- run has two different styles: extension function with receiver and function
a.) extension function with receiver <T, R> T.run(block: T.() -> R): R
Its use case seems like “I have won the stuff, I need to modify its essence to get my eventual result,” that’s why I regard T.run
as T.apply
+ T.let
. If context object T is treated as a Subject, we prefer to use run
. There is an excellent example from kotlinexpertise:
val date: Int = Calendar.getInstance().run {
set(Calendar.YEAR, 2030)
get(Calendar.DAY_OF_YEAR) //return value of run
}
Like the example above, I set the year of Calendar instance to 2030 to get the day count. So I use T.run
to modify it and transform it to day-of-year.
Moreover, there is one more use case I suggest to use run. It’s that we want to do a null check and then use the receiver, but we don’t wish the transform. Inside plaid, there is a good sample for this case:
private fun createBitmap() {
cutout?.run {
if (!isRecycled) {
recycle()
}
}
....
cutout = createBitmap(width, height).applyCanvas {
drawColor(foregroundColor)
drawText(text, textX, textY, textPaint)
}
}
b.) function <R> run(block: () -> R): R
I only use this run
to break out of forEach
. Kotlin’s return expression returns from the nearest enclosing function, that’s means inside forEach
there is no direct equivalent for break
. The only workaround is adding another nesting function and returning by label:
var isExisted = falserun loop@{
stores?.forEach {
if (storeCache.contains(it)) {
isExisted = true
return@loop // this return@loop will break the forEach
}
}
}