Let it be ?.

Photo by Louis Tsai on Unsplash
In this post we will go through one of the scope functions offered by Kotlin which is let and few misconceptions around its usage.
Note : There is nothing wrong in Kotlin documentation. It clearly documents how we should be using
let
in Kotlin. However, as developers sometime we still have tendency to write code as per our initial knowledge about some language features when we learnt them.
In this post we will cover how we shouldn't be using let
. Don't worry, I've been in same train where we've been using it like i'll mention below. I realized this recently when I came across this tweet from Zhuinden and reading replies to it. If you go through replies, you'd see some good points discussed about we generally tend to use let
incorrectly in few scenarios where it is not required at all. I agree with most of the points discussed in that thread and will try to cover them here.
TL;DR : Please avoid using
let
in control flow. Useif/else
and Kotlin's smart casting to your advantage.
Let's get started. We will go through one example each of both incorrect and correct usage of let
. We will first go through incorrect use followed by correct one.
Incorrect use
Control Flow
Scenario - When your function has a nullable value as input parameter and on the basis of its nullablility you want to show/hide some views in Android.
fun renderVideo(video: Video?) {
video?.let {
binding.playerContainer.isVisible = true
binding.videoMetadata.isVisible = true
binding.videoDuration.isVisible = true
binding.videoDuration.text = it.duration
} ?: run {
binding.playerContainer.isVisible = false
binding.videoMetadata.isVisible = false
binding.videoDuration.isVisible = false
binding.videoDuration.text = ""
}
}
There is nothing wrong in this code. It works as intended, right? However, there is one problem with it 😅
let
returns value of lambda result.
"let" returns value of lambda result.
Let's say someone in future adds a method call at the end of let
block like below:
fun renderVideo(video: Video?) {
video?.let {
binding.playerContainer.isVisible = true
binding.videoMetadata.isVisible = true
binding.videoDuration.isVisible = true
binding.videoDuration.text = it.duration
doAction() // It can return `null` as one of the value } ?: run {
binding.playerContainer.isVisible = false
binding.videoMetadata.isVisible = false
binding.videoDuration.isVisible = false
binding.videoDuration.text = ""
}
}
fun doAction() : String? {
...
}
Now, if we call method named doAction
which returns null inside let
block, it will cause our run
block to execute.
This is because let
returns value of lambda result which would be null
in this case.
null ?: run {
// code inside it runs
}
Ideally you wouldn't add a method call inside let
block like above. Fair point! But what if someone else maintaining this code in future isn't aware about this pitfall and adds it. And, then loses decent amount of time debugging why both the let
and run
block are running despite value of video
being non-null 🤬
let
and run
scope functions weren't created to be used in control flow like above. Using simple if/else
is readable and doesn't causes any issues down the line in future as well.
fun renderVideo(video: Video?) {
if(video != null){
binding.playerContainer.isVisible = true
binding.videoMetadata.isVisible = true
binding.videoDuration.isVisible = true
binding.videoDuration.text = video.duration
}
else{
binding.playerContainer.isVisible = false
binding.videoMetadata.isVisible = false
binding.videoDuration.isVisible = false
binding.videoDuration.text = ""
}
}
This sounds like both readable and obvious solution but why people still tend to always reach out for ?.let{}
in such situations. IMO we think that only if we write ?.let{}
the code written inside let
block is safely non-null on which the let
was called.
That is not case. We can do a null check on nullable val type and Kotlin compiler will always smart cast it to non-null type under it. We don't need to reach out to let
for it.
fun printInfo(person: Person?){
if(person != null){
// `Person` object is now smart casted to non-null type.
// And, it will be non-null inside this if scope.
println(person.name)
}
}
Note : Important thing is that it has to be
val
and notvar
because Kotlin compiler can't do a smart cast in that case. Generally when we receive this values in our function parameter they are alwaysval
becausevar
as a function parameter is not a thing in Kotlin.
Do we all agree that using old school if/else
is much better than being clever using let
and run
? Cool! 😄
Now, that we've talked about where we should avoid using let
, let's take a look at where we should be actually using it.
Correct use
Chaining methods calls
Let's say we want to write code where we need to do call some functions conditionally in sequence.
One such scenario :
- Get currently displayed product
- If there is a currently displayed product, fetch updated product details using its "id"
- If there is updated product details available, re-render the displayed product with updated product details.
Without let
this is how we would generally write it
fun renderProduct(){
val currentlyDisplayedProduct = productDrawerViewModel.getCurrentDisplayedProduct()
if(currentlyDisplayedProduct != null) {
val updatedProductDetails = viewModel.getUpdatedProductDetails(updatedProductDetails.id)
if(updatedProductDetails != null) {
productDrawerViewModel.renderProduct(updatedProductDetails)
}
}
}
How about using let
.
fun renderProduct(){
productDrawerViewModel.getCurrentDisplayedProduct()
?.let { displayedProduct -> viewModel.getUpdatedProductDetails(displayedProduct.id) }
?.let { updatedProductDetails -> productDrawerViewModel.renderProduct(updatedProductDetails) }
}
Using let
we can chain method calls, similar to how we can run map
on collections. IMO much more readable than previous nested if checks. We can use it to chain non-nullable results as well, it doesn't have to be ?.let
always but you got the point. This was the one use of let
I wanted to highlight. I'd recommend checking in Kotlin documentation for the other use cases of let
.
If you notice the difference, in 1st case we wanted to avoid using let
and run
like if/else
because of the issue discussed.
And, in 2nd case we're using let
to chain method calls to avoid nested if conditions.
We've been writing ?.let{} ?: run{}
in our codebase a lot instead of simple if/else
and need to fix it.
Thanks Zhuinden for sharing it! TIL(today i learned) and wanted to share it.
Brb! fixing our codebase 😜
Thanks for reading! 🙂