When we first encounter Kotlin's scope functions (let, run, apply, also, and with), they look almost identical. But each one has its own purpose, and once you understand their best use cases, you'll notice how they can simplify your code and make it more readable.

In this article, I'll walk you through each scope function with best practices, real-world Android examples, and when to use them.

Why Scope Functions Exist in the First Place?

Imagine you're configuring an object, working with a nullable variable, or chaining multiple calls. Without scope functions, your code might look repetitive:

val paint = Paint()
paint.color = Color.RED
paint.strokeWidth = 5f
paint.style = Paint.Style.FILL

With scope functions, you can make it shorter, more expressive, and avoid repeating variable names.

1. let → Work Safely with Nullable Objects

When to use:

  • For null-safety checks.
  • When you want to transform one object into another.
  • To limit a variable's scope.

Android Example (Handling nullable text):

val userName: String? = intent.getStringExtra("USER_NAME")
userName?.let {
    Toast.makeText(this, "Welcome $it!", Toast.LENGTH_SHORT).show()
}

Here, let ensures the toast only shows if userName is not null.

Use let when you want to work with a nullable or transform data into a different type.

2. run → Execute Multiple Operations and Return a Value

When to use:

  • For object initialization where you need the final computed value.
  • To wrap multiple statements and return the last one.

Android Example (Building a message):

val message = StringBuilder().run {
    append("Hello, ")
    append("Kotlin in Android!")
    toString()
}
Log.d("TAG", message)

Use run when you want to perform operations and get a result from them.

3. apply → Configure an Object

When to use:

  • When you want to initialize or configure an object.
  • Especially useful for UI elements or builders.

Android Example (Setting up a View):

val textView = TextView(this).apply {
    text = "Hello Android"
    textSize = 18f
    setTextColor(Color.BLACK)
}

Use apply when you want to set up an object without repeating its name.

4. also → Add Side Effects Without Breaking the Chain

When to use:

  • For debugging or logging.
  • When you want to perform some extra action without changing the object.

Android Example (Logging SharedPreferences update):

val sharedPrefs = getSharedPreferences("APP_PREFS", MODE_PRIVATE)
sharedPrefs.edit()
    .putString("TOKEN", "abcd1234")
    .also { Log.d("Prefs", "Saving token...") }
    .apply()

Use also when you want to keep the object the same but add some side effect like logging.

5. with → Perform Multiple Actions on an Object

When to use:

  • When you already have a non-null object.
  • To avoid repeating its name when calling multiple methods.

Android Example (Configuring a Paint object):

val paint = Paint()
val description = with(paint) {
    color = Color.BLUE
    strokeWidth = 10f
    style = Paint.Style.STROKE
    "Paint with color $color and stroke $strokeWidth"
}
Log.d("TAG", description)

👉 Use with when you want to call multiple functions on the same object and return a result.

Quick Summary

Final Thoughts

Scope functions are not just syntactic sugar — they make your Kotlin (and Android) code cleaner and less error-prone.

  • Reach for let when dealing with nullable types.
  • Use apply for object setup.
  • Use also for logging or side effects.
  • Use run when you want to compute and return something.
  • Use with when you already have an object and want to do multiple things with it.

If you start applying them in your Android projects, you'll quickly see how much boilerplate they remove.