When we first encounter Kotlin's scope functions (let
, run
, apply
, also
, and with
), they look almost identical. But each one has its own purpose, and once you understand their best use cases, you'll notice how they can simplify your code and make it more readable.
In this article, I'll walk you through each scope function with best practices, real-world Android examples, and when to use them.
Why Scope Functions Exist in the First Place?
Imagine you're configuring an object, working with a nullable variable, or chaining multiple calls. Without scope functions, your code might look repetitive:
val paint = Paint()
paint.color = Color.RED
paint.strokeWidth = 5f
paint.style = Paint.Style.FILL
With scope functions, you can make it shorter, more expressive, and avoid repeating variable names.
1. let
→ Work Safely with Nullable Objects
When to use:
- For null-safety checks.
- When you want to transform one object into another.
- To limit a variable's scope.
Android Example (Handling nullable text):
val userName: String? = intent.getStringExtra("USER_NAME")
userName?.let {
Toast.makeText(this, "Welcome $it!", Toast.LENGTH_SHORT).show()
}
Here, let
ensures the toast only shows if userName
is not null.
Use let
when you want to work with a nullable or transform data into a different type.
2. run
→ Execute Multiple Operations and Return a Value
When to use:
- For object initialization where you need the final computed value.
- To wrap multiple statements and return the last one.
Android Example (Building a message):
val message = StringBuilder().run {
append("Hello, ")
append("Kotlin in Android!")
toString()
}
Log.d("TAG", message)
Use run
when you want to perform operations and get a result from them.
3. apply
→ Configure an Object
When to use:
- When you want to initialize or configure an object.
- Especially useful for UI elements or builders.
Android Example (Setting up a View):
val textView = TextView(this).apply {
text = "Hello Android"
textSize = 18f
setTextColor(Color.BLACK)
}
Use apply
when you want to set up an object without repeating its name.
4. also
→ Add Side Effects Without Breaking the Chain
When to use:
- For debugging or logging.
- When you want to perform some extra action without changing the object.
Android Example (Logging SharedPreferences update):
val sharedPrefs = getSharedPreferences("APP_PREFS", MODE_PRIVATE)
sharedPrefs.edit()
.putString("TOKEN", "abcd1234")
.also { Log.d("Prefs", "Saving token...") }
.apply()
Use also
when you want to keep the object the same but add some side effect like logging.
5. with
→ Perform Multiple Actions on an Object
When to use:
- When you already have a non-null object.
- To avoid repeating its name when calling multiple methods.
Android Example (Configuring a Paint object):
val paint = Paint()
val description = with(paint) {
color = Color.BLUE
strokeWidth = 10f
style = Paint.Style.STROKE
"Paint with color $color and stroke $strokeWidth"
}
Log.d("TAG", description)
👉 Use with
when you want to call multiple functions on the same object and return a result.
Quick Summary
Final Thoughts
Scope functions are not just syntactic sugar — they make your Kotlin (and Android) code cleaner and less error-prone.
- Reach for
let
when dealing with nullable types. - Use
apply
for object setup. - Use
also
for logging or side effects. - Use
run
when you want to compute and return something. - Use
with
when you already have an object and want to do multiple things with it.
If you start applying them in your Android projects, you'll quickly see how much boilerplate they remove.