-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
208: Common Kotlin Interview Questions on Scope Functions
I've sat through plenty of technical interviews where the candidate knows the syntax of Kotlin, but they freeze the moment I ask, "Why would you use also instead of apply?" On paper, they look almost identical. In practice, using the wrong one can make your code read like a riddle. Let's actually break one and see where the lines are drawn.
data class UserSession(
var username: String = "",
var authToken: String = "",
var isAuthenticated: Boolean = false
)
The "Configuration" phase
Suppose I have a raw UserSession that I need to set up before passing it to a service. My first instinct is usually apply. I want to "apply" these settings to the object.
val session = UserSession().apply {
username = "dev_user"
authToken = "abc-123"
isAuthenticated = true
}
// 'session' is now a UserSession object
I'm inside the block, and I can access username directly. That's because apply uses this as the receiver. It's clean. But here's where the interview questions start: what if I want to log the session object while I'm initializing it, without confusing the configuration with the logging?
If I just put a println(this) inside apply, it works, but it feels "muddy." The apply block is for modifying the object. For side effects—things that don't change the object's state—I'll try also.
val session = UserSession().apply {
username = "dev_user"
}.also {
println("Session created for user: ${it.username}")
}
Notice the shift. In apply, I used this (implicitly). In also, I'm using it. This is a crucial distinction. also gives me the object as an argument, not as a receiver. In a real-world codebase, this prevents me from accidentally changing a value inside a block that's supposed to be "read-only" for logging or validation.
The return value trap
This is the part where most people stumble. I'm going to try and chain a let onto the end of my session setup to return a status message. Watch what happens to the type.
val result = UserSession().apply {
username = "dev_user"
}.let {
"User ${it.username} is now active"
}
println(result) // This prints the String, not the UserSession
I just changed the return type of the entire expression from a UserSession to a String. If I had used also instead of let, result would still be the UserSession object. In an interview, if you're asked the difference between let and also, don't just say "one uses it and one uses this." The real answer is about the return value.
applyandalsoreturn the object itself. (Great for chaining).letandrunreturn the result of the lambda. (Great for transformations).
Deciding between run and with
Now, what about run and with? They both use this. I'll try to use with to perform some operations on my session without assigning it to a variable first.
val session = UserSession("admin", "secret", true)
with(session) {
println("Checking session for $username...")
// I can access username directly because 'this' is the session
}
Now I'll try run. It looks almost exactly the same, but the syntax is slightly different.
session.run {
println("Checking session for $username...")
}
Why do both exist? I've found that with is a standalone function—you pass the object in. run is an extension function. This means run is the winner when you need to handle nullability. I can't do with(session?) { ... } and expect it to skip the block if it's null. But I can do session?.run { ... }.
If I'm in an interview, I summarize it like this: with is for "with this object, do the following," while run is for "take this object and execute this logic on it, potentially returning a result."
📋 Practical Task
Refactoring the API Response Handler
You have a legacy piece of code that handles a nullable ApiResponse. It currently uses a messy set of if checks and temporary variables. Your goal is to refactor this into a clean chain using scope functions.
The Requirements:
- Use
letto handle the nullability of theresponse. - Inside that block, use
applyto set theprocessedTimestampof the response. - Chain an
alsoblock to log the final processed response to the console. - The final result of the entire expression should be the
ApiResponseobject itself (not a string or null).
data class ApiResponse(
val data: String,
var processedTimestamp: Long = 0
)
fun handleResponse(response: ApiResponse?) {
// TODO: Refactor the following logic into a single scope function chain
if (response != null) {
response.processedTimestamp = System.currentTimeMillis()
println("Log: Processed response: ${response.data}")
// Imagine this is passed to another function here
}
}There are no comments for now.