Let it be ?.

December 05, 2021 ■ 3 min read
article banner

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. Use if/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 not var because Kotlin compiler can't do a smart cast in that case. Generally when we receive this values in our function parameter they are always val because var 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! 🙂

Want to be notified of similar posts? Follow me @punit__d on Twitter 🙂