Let it be ?.
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 with the Kotlin documentation. It documents well how we should be using
let
in Kotlin. However, as developers, sometimes we still write code based on 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 on the same train where we've used it wrong, as 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 responses, you'd see some good points discussed how we generally use let
incorrectly in a few scenarios where it is not required. 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 the control flow. Useif/else
and Kotlin's smart casting.
Let's get started. We will go through both examples of incorrect and correct usage of let
. We will first go through incorrect use followed by correct one.
Incorrect use - Using it in Control Flow
Scenario: When your function has a nullable value as an input parameter, you want to show/hide some views in Android based on its nullability.
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 with this code. It works as intended, right? However, there is one problem with it 😅
let
returns the value of the lambda result.
Let's say someone in future adds a method call at the end of the 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()
} ?: run {
binding.playerContainer.isVisible = false
binding.videoMetadata.isVisible = false
binding.videoDuration.isVisible = false
binding.videoDuration.text = ""
}
}
fun doAction() : String? {
...
}
Now, if we call a method named doAction
which returns null inside the let
block, it will also cause our run
block to execute as well.
Remember, let
returns the value of the lambda result, which would be null
in this case.
null ?: run {
// code inside it runs
}
Ideally, you wouldn't add a method call inside the let
block like above. Fair point! But what if someone else maintaining this code in future is unaware of this pitfall and adds it? And, then, loses a decent amount of time debugging why both the let
and run
blocks are running despite the value of video
being non-null 🤬
let
and run
scope functions weren't created to be used in the control flow like above. A simple if/else
is readable and doesn't cause 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 = ""
}
}
IMO this is a more readable and straightforward solution, but why do people still tend to always reach out for ?.let{}
in such situations? This is because we think that only if we write ?.let{}
the code written inside the let
block is safely non-null on which the let
was called.
That is not the case. We can do a null check on the nullable val type and the Kotlin compiler will always smart-cast it to a 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: The important thing is that it has to be
val
and notvar
because the Kotlin compiler can't do a smart cast in that case. Generally, when we receive these 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 look at where we should actually be using it.
Correct use - Chaining methods calls
Let's say we want to write code where we need to call some functions conditionally in sequence.
Scenario :
- Get the currently displayed product
- If there is a currently displayed product, fetch updated product details using its "id"
- If updated product details are 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) }
}
We can chain method calls using let
, similar to how we run map
on collections. IMO much more readable than previous nested if checks. We can also use it to chain non-nullable results; 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
.
To summarize, in the 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 incorrectly using ?.let{} ?: run{}
a lot in our codebase, instead of a simple if/else
, and we need to fix it. Brb! fixing our codebase 😜
Thanks for reading! 🙂