Kotlin Scope Functions

Scope functions execute a block of code within the context of an object. They do not add new behavior — they improve how you write code that uses an object. Kotlin has five scope functions: let, run, with, apply, and also.

Why Scope Functions Exist

Without scope functions you often repeat the object's name multiple times:

val user = User()
user.name = "Riya"
user.age = 25
user.city = "Mumbai"
println(user)

With apply, you write it once and configure inside a block:

val user = User().apply {
    name = "Riya"
    age = 25
    city = "Mumbai"
}
println(user)

The Five Scope Functions at a Glance

Function | Object ref  | Returns        | Common use
---------|-------------|----------------|-------------------------------
let      | it          | Lambda result  | Null check + transform
run      | this        | Lambda result  | Compute a value using the object
with     | this        | Lambda result  | Operations on an object (non-null)
apply    | this        | The object     | Configure/initialize an object
also     | it          | The object     | Side effects (logging, validation)

let — Transform and Null-Check

Use let to work with a value, especially nullable ones. The object is referred to as it inside the block. The block's last expression is returned.

val name: String? = "Kotlin"

val result = name?.let {
    println("Processing: $it")
    it.uppercase()
}
println(result)   // KOTLIN

val nullName: String? = null
val r2 = nullName?.let { it.uppercase() }
println(r2)   // null (block never ran)

run — Compute a Result Using the Object

run combines object setup and result computation in one block. The object is this inside the block.

data class Circle(val radius: Double)

val area = Circle(5.0).run {
    val pi = 3.14159
    pi * radius * radius   // last expression is returned
}
println(area)   // 78.53975

run also works without a receiver — use it to create an isolated scope for a multi-step calculation:

val total = run {
    val tax = 200.0
    val base = 1000.0
    base + tax   // returns 1200.0
}
println(total)   // 1200.0

with — Work With an Object You Already Have

with is a regular function (not an extension). Pass the object as an argument and work with it as this inside the block.

data class Printer(val brand: String, val model: String, var ink: Int)

val printer = Printer("HP", "LaserJet", 80)

with(printer) {
    println("Brand: $brand")
    println("Model: $model")
    println("Ink: $ink%")
}

apply — Configure an Object

apply runs a setup block on an object and returns the same object. The object is this inside the block. Use it to initialize or configure an object just after creating it.

data class Config(var host: String = "", var port: Int = 0, var secure: Boolean = false)

val config = Config().apply {
    host = "api.estudy247.com"
    port = 443
    secure = true
}
println(config)
// Config(host=api.estudy247.com, port=443, secure=true)

apply Chaining

val message = StringBuilder()
    .apply { append("Hello") }
    .apply { append(", ") }
    .apply { append("World!") }
    .toString()

println(message)   // Hello, World!

also — Side Effects Without Changing Flow

also runs a block and returns the original object unchanged. The object is it inside the block. Use it for logging, validation, or debugging without breaking a chain of operations.

val scores = mutableListOf(85, 92, 78)
    .also { println("Before sort: $it") }
    .sorted()
    .also { println("After sort: $it") }

// Before sort: [85, 92, 78]
// After sort: [78, 85, 92]

Choosing the Right Scope Function

Question                                  | Use
------------------------------------------|-------
Do a null-check and transform the value?  | let
Configure a new object during creation?   | apply
Compute a result from an object?          | run
Log or validate without changing flow?    | also
Call multiple methods on a non-null obj?  | with

Diagram — return value and object reference

                 Object reference    Returns
                 inside block
               ┌──────────────────────────────┐
let            │    it (lambda param)         │  Lambda result
run            │    this (receiver)           │  Lambda result
with           │    this (argument)           │  Lambda result
apply          │    this (receiver)           │  The object itself
also           │    it (lambda param)         │  The object itself
               └──────────────────────────────┘

Real-World Example — Building an Android Intent

// With apply, Android setup code becomes clean and readable
val intent = Intent(context, DetailActivity::class.java).apply {
    putExtra("userId", 42)
    putExtra("screen", "profile")
    flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
}
startActivity(intent)

Leave a Comment

Your email address will not be published. Required fields are marked *