Kotlin
Completed
-
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
29: Scope Functions: let, run, apply, also, with
I've been looking at some legacy code in a project I'm maintaining, and I keep seeing this pattern where a variable is repeated over and over again just to set some properties. It's tedious to read and feels... un-Kotlin-y. Let's take a common scenario: configuring a UserAccount object. I'll write it the "boring" way first and see where we can trim the fat.
class UserAccount {
var username: String = ""
var email: String = ""
var isVerified: Boolean = false
}
fun setupAccount() {
val account = UserAccount()
account.username = "kotlin_dev"
account.email = "dev@example.com"
account.isVerified = true
println("Account setup complete for ${account.username}")
}
Cleaning up the repetition
See that? account.something, account.somethingElse. It's redundant. I want a way to "enter" the context of that object and just set the properties. I remember apply is designed for exactly this. Let's swap it in.
fun setupAccount() {
val account = UserAccount().apply {
username = "kotlin_dev"
email = "dev@example.com"
isVerified = true
}
println("Account setup complete for ${account.username}")
}
That's much cleaner. Inside the apply block, the object is referred to as this (though we can omit it). The key thing to notice here is that apply returns the object itself. That's why I can still assign the result to val account. It's a "configure and return" function.
Adding a side-effect without messing with the object
Now, what if I want to log the account creation for debugging, but I don't want the logging logic mixed in with the property assignments? I could just put a println inside apply, but that feels messy. I want a separate step that says, "Do this other thing with the object, but then give me the object back."
This is where also comes in. I'll chain it onto the apply block.
val account = UserAccount().apply {
username = "kotlin_dev"
email = "dev@example.com"
isVerified = true
}.also {
println("Log: Created account with email ${it.email}")
}
Wait, why did I use it here instead of this? That's the main difference between apply and also. apply uses a receiver (this), while also uses a lambda argument (it). I personally prefer also for logging because it makes it explicit that I'm acting *on* the object rather than configuring it. Like apply, also returns the original object.
Dealing with the "maybe" data
Let's pivot. Imagine our UserAccount had a nullable profilePictureUrl. I only want to trigger a download if that URL actually exists. I could use an if check, but let is the idiomatic way to handle this.
val profilePictureUrl: String? = "https://cdn.com/pic.jpg"
profilePictureUrl?.let { url ->
println("Downloading image from $url...")
// Imagine a download function here
}
Unlike apply or also, let doesn't necessarily return the object it started with. It returns the result of the last line in the block. In the example above, it returns Unit (nothing). But if I wanted to transform that URL into a File object, let would be my go-to.
Combining configuration and calculation
Sometimes I need to configure an object and then immediately compute something based on that configuration, returning a value instead of the object itself. If I use apply, I get the object back. If I want the result of a calculation, I should use run.
val accountStatus = UserAccount().run {
username = "kotlin_dev"
isVerified = true
// The last line is the return value
if (isVerified) "Active" else "Pending"
}
println("Status is: $accountStatus") // Prints "Active"
run is like a hybrid. It uses this (like apply) but returns the lambda result (like let). It's great for "initialize and calculate."
When the object is already initialized
Finally, there's with. It's very similar to run, but instead of being called on the object (object.run { ... }), you pass the object into it. I usually use with when I have an object that's already been created and I just want to group a bunch of calls to it together without typing the variable name ten times.
val account = UserAccount()
// ... later in the code ...
with(account) {
println("User: $username")
println("Email: $email")
println("Verified: $isVerified")
}
It doesn't return the object; it returns the result of the block. It's basically a way of saying, "With this object, do the following."
To wrap this up, here is my mental cheat sheet for picking the right one:
- apply: "I want to configure this object and get it back." (Receiver:
this, Returns: Object) - also: "I want to do something extra (like logging) and get the object back." (Argument:
it, Returns: Object) - let: "I want to execute this only if the object isn't null, or I want to transform it." (Argument:
it, Returns: Result) - run: "I want to configure this and then calculate a result." (Receiver:
this, Returns: Result) - with: "I already have this object; let me group these calls together." (Receiver:
this, Returns: Result)
📋 Practical Task
Refactoring the GameCharacter Setup
You are working on a game engine. You have a GameCharacter class with several properties. Currently, the character initialization is verbose and uses a lot of repetitive variable references. Your task is to refactor the initializeHero function using the appropriate scope functions.
class GameCharacter {
var name: String = ""
var health: Int = 0
var strength: Int = 0
var weapon: String? = null
}
fun initializeHero() {
val hero = GameCharacter()
hero.name = "Alaric"
hero.health = 100
hero.strength = 15
// Log that the hero was created
println("Hero ${hero.name} has entered the game!")
// If the hero has a weapon, print it, otherwise print "unarmed"
val weaponStatus = if (hero.weapon != null) {
"Equipped with ${hero.weapon}"
} else {
"Unarmed"
}
println(weaponStatus)
}
Requirements:
- Use
applyto set thename,health, andstrength. - Use
alsoto handle the "Hero has entered the game" logging. - Use
letto handle theweaponStatuslogic for the nullableweaponproperty.
There are no comments for now.