Kotlin Scope Functions: let, run, with, apply, also — Complete Guide
Kotlin's scope functions are powerful tools that let you execute code blocks in the context of an object. They make your code more concise and readable. Let's master all five!
The Five Scope Functions at a Glance
| Function | Receiver | Return Value | Best For |
|---|---|---|---|
let |
it |
Last expression | Null safety checks, transformations |
run |
this |
Last expression | Initialization + computation |
with |
this |
Last expression | Working with object properties |
apply |
this |
Same object | Builder pattern, fluent configuration |
also |
it |
Same object | Side effects, logging, debugging |
1. let: Null Safety & Transformations
val name: String? = "Alice"
// Without let: repetitive null checks
if (name != null) {
println(name.uppercase())
}
// With let: elegant null safety
name?.let {
println(it.uppercase()) // it = "Alice"
}
// Transform values
val length = name?.let { it.length } ?: 0
Use let when:
- Checking null-safe operations
- Transforming object values
- Avoiding temporary variables
2. run: Initialize + Compute
// Configure and compute in one block
val message = run {
val prefix = "Hello, "
val name = "Kotlin"
prefix + name // Last expression returned
}
// Complex initialization
class Database {
fun connect() = println("Connected")
fun query(sql: String) = "Results"
}
val result = Database().run {
connect()
query("SELECT * FROM users") // Returned as result
}
Use run when:
- Need to execute multiple statements and return a computed value
- Setting up a temporary object with complex logic
3. with: Direct Property Access
data class User(val name: String, val age: Int, val email: String)
val user = User("Bob", 30, "bob@example.com")
// Without with: repetitive user.
val profile = user.name + " is " + user.age + " years old"
// With with: clean property access
val profile = with(user) {
"$name is $age years old, reach me at $email"
}
Use with when:
- Working with multiple properties of an object
- Don't need to return the object itself
- Prefer readability over fluent chaining
4. apply: Builder Pattern
// Building a data class with fluent syntax
data class Button(
var text: String = "",
var onClick: (() -> Unit)? = null,
var enabled: Boolean = true
)
val button = Button().apply {
text = "Click Me"
enabled = true
onClick = { println("Clicked!") }
}
// Real example: SharedPreferences
val sharedPref = context.getSharedPreferences("config", Context.MODE_PRIVATE)
sharedPref.edit().apply {
putString("username", "Alice")
putInt("theme", 1)
commit() // Can call methods too
}
Use apply when:
- Configuring objects in a builder style
- Want to return the same object after setup
- Setting multiple properties at once
5. also: Side Effects & Logging
val numbers = listOf(1, 2, 3, 4, 5)
numbers
.filter { it > 2 }
.also { println("Filtered: $it") } // Log intermediate result
.map { it * 2 }
.also { println("Mapped: $it") }
.forEach { println(it) }
// Debugging chains
val user = getUserData()
.also { println("User loaded: ${it.name}") }
.also { it.validate() }
.also { println("User validated") }
Use also when:
- Need side effects (logging, validation, metrics)
- Want to return the original object (unlike run)
- Debugging chains of transformations
Practical Combinations
Null-safe initialization with logging
val config: Map<String, String>? = loadConfig()
config?.let { cfg ->
cfg.also { println("Config loaded with ${it.size} keys") }
.apply { println("Ready for use") }
}
Builder + logging pattern
val request = HttpRequest().apply {
url = "https://api.example.com/users"
method = "GET"
timeout = 5000
}.also {
println("Request prepared: ${it.url}")
}
Chaining transformations
val names = listOf("alice", "bob", "charlie")
names
.map { it.uppercase() }
.also { println("Uppercase: $it") }
.filter { it.length > 3 }
.also { println("Filtered: $it") }
.forEach { println(it) }
Decision Flowchart
Need to return object itself?
├─ YES → apply (configure) or also (side effects)
└─ NO ─→ Need 'this' receiver?
├─ YES → run (multi-statement) or with (property-heavy)
└─ NO ─→ let (use 'it', null-safe, transform)
Key Takeaways
- let: Transform and handle nulls
- run: Multi-statement initialization
- with: Clean property access (not chainable)
- apply: Fluent configuration
- also: Debugging and side effects
Master these five, and your Kotlin code will be cleaner, more readable, and more maintainable!
Level up your Android skills! Get 8 Android App Templates → https://myougatheax.gumroad.com
Top comments (0)