Noisy Code 🗣 With Kotlin Scopes
Scopes make your code more readable? think again
Featured on
Kotlin Weekly

Android Weekly

ProAndroidDev

Daily Dev

Others
AndroidBoss #100 | onCreate Digest | Kotlin trends

You are going to encounter these scope functions namely let, run, apply, also, with
in 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
andimage
, 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.
Enjoying the Post?
a clap is much appreciated if you enjoyed. No sign up or cost associated :)
If you want to support my content do consider dropping a tip/coffee/donation💰
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
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 insidealso
, theintent
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 thefunction
is really clean, definitely works best to control chaining.
fun startSomeActivity() {
startActivity(getSomeIntent())
}
fun getSomeIntent() = Intent(context,SomeActivity::class.java).apply {
//....
}
Enjoying the Post?
a clap is much appreciated if you enjoyed. No sign up or cost associated :)
If you want to support my content do consider dropping a tip/coffee/donation💰
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! 👩💻
Enjoying the Post?
a clap is much appreciated if you enjoyed. No sign up or cost associated :)
If you want to support my content do consider dropping a tip/coffee/donation💰