Noisy Code 🗣 With Kotlin Scopes

scope functions Apr 08, 2021

Scopes make your code more readable? think again



Kotlin Weekly

Kotlin Weekly #245

Android Weekly

Android Weekly - Free weekly Android & Kotlin development newsletter
Android Weekly - Free weekly Android & Kotlin development newsletter

ProAndroidDev

Noisy Code 🗣 with Kotlin Scopes
You are going to encounter these scope functions namely let, run, apply, also, within every Kotlin codebase, along with all the mischievous ways developers exploit their usage from the way they were…

Daily Dev

Others

AndroidBoss #100  | onCreate Digest |  Kotlin trends

You are going to encounter these scope functions namely let, run, apply, also, within every Kotlin codebase, along with all the mischievous ways developers exploit their usage from the way they were intended for. Let see how popular opinion on those ends up just as a code noise.


Let me out! ⛓

Popular opinion → let is treated as a native way to do null checks on the variables.

fun deleteImage(){
   var imageFile : File ? = ...
   imageFile?.let { // ❌ don't write code like this
      if(it.exists()) it.delete()
   }
}

Please don’t do that because using let feels functionally beautiful but

  • You will end up creating another reference called it in the same scope which is less descriptive
  • To fix that you will definitely try to provide an explicit parameter for lambda example imageFile?.let{ image,
  • but it creates a load of find another name for the same variable (naming things are hard)
  • Now other devs can refer to image files in the scope with two names imageFile and image, with time you need to maintain that the right variable is being used inside the scope of lambda when there is iteration over the code for bugs and changes.

We can avoid these and other hiccups just by doing normal null checks.

fun deleteImage(){
   val imageFile : File ? = ...
   if(imageFile != null) {
      // 👇 smart casted imageFile into non nullable
      if(imageFile.exists()) imageFile.delete()
   }
}

And for guys who are too smart will argue that smart casting won’t work on global fields. I will say read my article 👉 [lateinit vs nullable types | global fields] or watch my video 👇

And stop polluting your global space.

The reason why smart cast fails because the global field can be accessed by other functions of the same class. In any async situation, global value can update at any time so it’s impossible to smart cast.

So, to fix that you can use

class Foo {
  var imageFile : File ? = ...
  fun deleteImage(){
     val imageFile = this.imageFile
     if(imageFile != null) {
        if(imageFile.exists()) imageFile.delete()
     }
  }
}

Since you will end up making a new variable anyways which might be of a different name, you can get away with using let in such cases.

PS: don’t forget to make it descriptive

class Foo {
  var imageFile : File ? = ...
  fun deleteImage(){
      // 🧐 you can get away for now
      imageFile?.let { image ->
        if(image.exists()) image.delete()
      }
  }
}

Another Bonus part …

  • Nesting scoped function is considered bad practice even Kotlin style guide point this out.
  • Prefer using if for null checks then you can use scope operators where it is actually required.

Community Feedback

Thanks, Joost Klitsie Provides better alternates regarding the use of let, I definitely loved all the approaches.

// alternate 1 
fun deleteImage() {
  val imageFile = getImage() ?: return
  ...
}

//---- OR -----
// alternate 2
fun deleteImage() {
   getImage()?.takeIf { it.exists }?.let {it.delete()}
}

//---- OR -----
// alternate 3
fun deleteImage() {
 val image = getImage()?.takeIf { it.exists } ?: return
 image.delete()
}

In this way avoided assigning a new value to a variable in the function scope, so I do not have the double name problem.



Also, Apply my rule! 👑

Edit 1: This section has been updated after the feedback from

Joost Klitsie.

Popular opinion → Create scope to isolate common functionalities to modularize your code.

Intent(context, MyActivity::class.java).apply {
    // scope to configure object
    putExtra("data", 123)
    addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}).also { intent ->
    // scope to consume the object
    startActivity(this@FooActivity,intent)
}


The pure functions|lambdas generally are expected to be one-liner expression. This has been explained really well by Sir Venkat Subramaniam in one of his talks about functional programming, highly recommended to consume all of his work!

TLDR

  • Lambdas shine out in making code concise by still retaining the expressive parts of it.
  • Generally, lambdas with huge function bodies end up hindering the readability of the functionally chained pipeline you want to create.

So chaining with small lambdas doesn't disrupt the readability of your code.

From the above example

  • The scope of intent is limited inside also, the intent is restricted to that lambda itself
  • This will make you add all the code that needs to access intent into the same scope
  • It will not take a lot of time when also lambda becomes a huge mess

A simple fix to it … Flatten your hierarchy when possible

val intent = Intent(context, MyActivity::class.java).apply {
    // scope to configure object
    putExtra("data", 123)
    addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
})
startActivity(this@FooActivity,intent)

Flattened and not scary anymore, there was no point in making another scope introduce another scope for just consume the object.

Community Feedback

Thanks, Joost Klitsie for your insights. Your example on abstracting out into thefunctionis really clean, definitely works best to control chaining.

fun startSomeActivity() {
 startActivity(getSomeIntent())
}
fun getSomeIntent() = Intent(context,SomeActivity::class.java).apply {
  //....
}



Run from Run ?: 🏃🏻‍♂️💨

There is two run in Kotlin one is a scope/extension function and the other is a normal higher-order function.

popular opinion → It’s commonly used to execute some statement.

val userName: String
    get() {
        preferenceManager.getInstance()?.run { 
            getName() 
        } ?: run {
            getString(R.string.stranger)
        }
    }
    

Wtf is this?… absolute disappointment I suppose.

  • Absolutely yes you can chain scoped operators like this but should we do it? NO!
  • Not many devs are aware that there are two types of run in the language and having both of them chained using Elvis operator is a complete disaster.
  • It adds no value to the code, just branching and noise
val userName: String
    get() = preferenceManager.getInstance()?.getName() ?: getString(R.string.stranger)

And smart devs who are thinking that this pattern might be useful when you have more line of code there instead of the one-liner, go and read Apply my rule section again! else it’s a democratic country do whatever you want.

Community Feedback

Joost Klitsie advice to come up with some helper extension functions you can use throughout the project, or use Kotlin native one.

preferenceManager.getInstance()?.getName().orDefault {
  Log.w(“Name not found, returning default”)
  getString(R.string.stranger)
}

Which really is a good way to architect your code, here the point of focus is to apply a pragmatic approach instead of chaining scopes mindlessly. Use your tools wisely.



You are not With me 😭

The most ignored one, and not an extension function.

val binding = MainLayoutBinding.inflate(layoutInflater)
with(binding) {
    textName.text = "ch8n"
    textTwitter.text = "twitter@ch8n2"
})

Devs often ignore with() for two reasons

  • It takes receiver in parameter, not in extension, so people can’t do function chaining with using it.
  • Secondly, doesn’t go well with nullable types receiver

That’s really sad, with is a really useful scope operator. IMO, it has very special a use-case, cause it's not good for receiving parameter and make function chains but it is very useful when you want to set the scope of the variable into the entire function body, see this example

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    val binding = MainLayoutBinding.inflate(layoutInflater)
    setup(binding)
}
// write binding once and forget ✅
fun setup(binding:MainLayoutBinding) = with(binding) {
    textName.text = "ch8n"
    textTwitter.text = "twitter@ch8n2"
}

// similar to withContext in coroutines ✅
suspend fun foo() = withContext(Dispachers.IO){
  // .. do stuff
}

If you compare it with other scoped operator example :

...
// it looks ugly 😵 IMO
fun setup(binding:MainLayoutBinding?) = binding?.run {
    textName.text = "ch8n"
    textTwitter.text = "twitter@ch8n2"
}

Anyway this ugly is an example of how to cater to nullable types as well, you can use run with nullable here, but I will highly suggest not to pass nullable parameters into your functions.


Community Feedback

Joost Klitsie advice to use apply , as it’s not returning the run result. That would work better than run absolutely agreed.



Conclusion 💆🏻‍♀️

In isolation using these extensions does look sensible and pragmatic but as whole they are not readable or maintainable, we lose the intent of what we wanted to modularize and so many blocks make it really hard especially in code reviews, always leverage Kotlin features where they do good.

Hope you find it informative and if you have any feedback or post request or want to subscribe to my mailing list forms are below.

Do consider Clap to show your appreciation, until next time. Happy Hacking! 👩‍💻


Enjoy the article?

a clap is much appreciated if you enjoyed. No sign up or cost associated :)







Chetan gupta

Hi there! call me Ch8n, I'm a mobile technology enthusiast! love Android #kotlinAlltheWay, CodingMantra: #cleanCoder #TDD #SOLID #designpatterns

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.