Mastering Kotlin Scope Functions: let, run, with, apply, also
Table of Contents
🧐 The Problem They Solve (Language Design Theory)
In Java and other imperative languages, we often find ourselves repeating a variable name to perform multiple operations on it, or creating unnecessary temporary variables.
// Java Style
User user = new User();
user.setName("Alice");
user.setAge(25);
user.setEmail("alice@example.com");
repository.save(user);
Kotlin introduces Scope Functions to solve this by creating a temporary “mini-scope” where the context object is accessible implicitly (like this or it).
The Magic Quadrant
To understand them, don’t memorize. Understand the two dimensions that differentiate them:
- How do I access the object?
this: The object is the receiver of the lambda (implicit Extension Function).it: The object is the argument of the lambda.
- What does the function return?
Context Object: Returns the object itself (good for chaining).Lambda Result: Returns whatever the last line of the lambda returns (good for transformation).
| Returns Context Object | Returns Lambda Result | |
|---|---|---|
Object as this | apply | run, with |
Object as it | also | let |
🛠️ Deep Dive into Each Function
1. let: The Nullability Transformer
let is the Swiss Army knife. It takes it and returns the lambda result.
Main Use: Execute a block only if a variable is not null.
val user: User? = repository.findUser("123")
// Without let
if (user != null) {
sendEmail(user.email)
}
// With let
user?.let {
sendEmail(it.email)
}
Functional Pattern: Transformations.
val userDto = user?.let { userMapper.toDto(it) } ?: UserDto.Empty
2. apply: The Configurator
apply takes this and returns the object itself. Perfect for initializing objects or configuring builders.
Main Use: Post-construction initialization.
// Android Intent configuration
val intent = Intent(context, DetailActivity::class.java).apply {
putExtra("ID", 123)
putExtra("MODE", "EDIT")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
// 'intent' is already configured and ready to use
3. run: The Execution Block
run is like let (returns result), but uses this.
Main Use: Compute a value based on object properties and return it.
val passwordHash = user.run {
// I can access 'name' and 'email' directly without 'it'
val salt = generateSalt(name)
hash(email + salt)
}
There is also run without a receiver object (simply creates a scope).
val result = run {
val x = 10
val y = 20
x + y
}
4. also: The Side Effect
also is like apply (returns object), but uses it. Its name says it all: “Do this… and also this other thing”.
Main Use: Logging or intermediate validations in a chain, without breaking the flow.
val user = createUser()
.also { logger.info("User created: ${it.id}") } // Side effect
.apply { role = "ADMIN" } // Configuration
If we used apply for the log, we would have to write logger.info("... ${this.id}"), which is valid but also makes it clearer that we are not modifying the object, just “looking” at it.
5. with: The Typing Saver
with is not an extension function, it is called as a normal function: with(object) { ... }.
Main Use: Grouping function calls on the same object.
with(binding) {
titleView.text = "Hello"
subtitleView.text = "World"
submitButton.setOnClickListener { ... }
}
🚫 Anti-Patterns and Dangers
Scope Functions are addictive. Use them in moderation.
1. The Hell of Nested it (Shadowing)
user?.let {
// it is user
it.address?.let {
// it is address, user is hidden
it.city?.let {
// it is city... Whose address was it?
}
}
}
Solution: Use explicit names in nested lambdas.
user?.let { user ->
user.address?.let { address ->
...
}
}
2. Mutating in let or run
If you are going to mutate the object’s state, use apply or also. If you use let, the reader expects a transformation, not a mutation. Be semantic.
3. Chains Too Long
If you chain 5 scope functions, the code becomes unreadable. Sometimes, a classic temporary variable is clearer and easier to debug.
🎯 Quick Decision Guide
- Is it a
nullcheck? ->let - Is it configuring a new object? ->
apply - Is it configuring and returning a different result? ->
run - Is it an intermediate log? ->
also - Is it grouping method calls? ->
with
Mastering these functions allows you to write more idiomatic, expressive, and concise Kotlin code.
You might also be interested in
Null Safety in Kotlin: The End of the 'One Billion Dollar Mistake'
Deeply understand Kotlin's type system, how it eliminates NullPointerExceptions, and advanced techniques for handling nullability elegantly.
The Billion Dollar Mistake: Null Safety in Kotlin vs Java
Why is 'null' so dangerous? Discover how Kotlin solves the NullPointerException problem at the compiler level and how to design null-safe APIs.
Clean Architecture: The Ultimate Guide for Modern Android
Demystifying Clean Architecture: A deep dive into layers, dependencies, and data flow to build indestructible Android apps.