Mastering Kotlin Scope Functions: A Comprehensive Guide

Rizwanul Haque
4 min readNov 15, 2023

--

Kotlin, a modern programming language for the JVM, introduces a set of powerful features that enhance code conciseness and readability. Among these features, Kotlin’s scope functions — let, also, run, apply, and with — provide a concise and expressive way to structure code blocks, reducing boilerplate and improving the overall code quality.

They let you perform actions on an object in a temporary scope, allowing access without using the object’s name.

Overview of Kotlin Scope Functions:

1. let Function:

  • Purpose: Executes a block of code on a non-null object.
  • Usage: Useful for performing operations on an object and handling null scenarios.
  • Return Value: Result of the last expression in the block or a custom result specified using the let function.
val result = someObject?.let {
// Code to execute on non-null object
it.doSomething()
}

2. also Function:

  • Purpose: Performs additional actions on an object without affecting its value.
  • Usage: Suitable for logging, side effects, or any action that doesn’t alter the object’s state.
  • Return Value: The original object on which the also function is called.
someObject.also {
// Additional actions
println("Performing additional actions on $it")
}.doSomething()

3. run Function:

  • Purpose: Executes a block of code on an object, similar to let, but allows both it and this references.
  • Usage: Useful for situations where you need to perform multiple operations on an object.
  • Return Value: Result of the last expression in the block or a custom result specified using the run function.
val result = someObject.run {
// Code to execute on the object
doSomething()
anotherFunction()
}

4. apply Function:

  • Purpose: Configures properties of an object. The result is the object itself.
  • Usage: Ideal for initializing or configuring properties of an object.
  • Return Value: The original object on which the apply function is called.
val result = someObject.apply {
// Configuration code
property1 = value1
property2 = value2
}

5. with Function:

  • Purpose: Similar to apply, configures properties of an object but is not an extension function and requires an explicit reference to the object.
  • Usage: Appropriate for configuring properties of an object without creating an extension function.
  • Return Value: Result of the last expression in the block or a custom result specified using the with function.
val result = with(someObject) {
// Configuration code
property1 = value1
property2 = value2
}

Key Distinctions:

1. Return Values:

  • let, run, and with return the result of the last expression in the block or a custom result.
  • also and apply return the original object on which the function is called.

2. Usage in Null Safety:

  • let is often used for null-checks, executing code only when the object is non-null.
  • also and apply don't perform null-checks and can be used irrespective of the object's nullability.

3. References:

  • run and let use the reference it to represent the current object.
  • with uses an explicit reference to the object passed as an argument.
  • also and apply do not provide a reference to the object inside the block.

Practical Use Cases:

1. let: Null Safety and Data Transformation

val userInput: String? = getUserInput()

val formattedInput = userInput?.let { input ->
// Use let to transform and format non-null input
"Formatted: $input"
} ?: run {
// Handle null case
"Default Value"
}

In this use case, let is employed for null safety and data transformation. If userInput is not null, the let block transforms and formats the input. If userInput is null, the run block provides a default value.

2. also: Logging or Side Effects

val numbers = mutableListOf(1, 2, 3)

numbers.also {
// Use also for logging or side effects without modifying the original list
println("Before Modification: $it")
}.add(4)

The also block is utilized for logging or side effects without modifying the original list. The list is then modified with the addition of the number 4.

3. run: Complex Operations on an Object


data class Person(var name: String, var age: Int)

val person = Person("John", 25)

val updatedPerson = person.run {
// Use run for multiple operations on the person object
name = "John Doe"
age += 5
this // Return the modified object
}

In this scenario, run is employed for complex operations on an object. The block allows multiple operations on the person object, updating its name and age. The result is the modified person object.

4. apply: Fluent API for Object Initialization

data class Car(var make: String, var model: String, var year: Int)

val newCar = Car("Toyota", "Camry", 2022).apply {
// Use apply for configuring properties during object initialization
year = 2023
// Other property configurations
}

Here, apply is used for a fluent API during object initialization. The block configures properties of the newCar object, allowing for a concise and expressive setup.

5. with: Configuring Properties Without Extension Function

// Use Case: Configuring Properties Without Extension Function

data class Book(var title: String, var author: String, var pages: Int)

val myBook = Book("The Kotlin Journey", "Jane Doe", 200)

with(myBook) {
// Use with for configuring properties without creating an extension function
title = "The Kotlin Adventure"
pages += 50
}

In this use case, with is applied for configuring properties of the myBook object without creating an extension function. The block allows concise property configuration, enhancing readability.

Summary

Kotlin’s scope functions are a valuable addition to the language, offering a versatile set of tools for managing code blocks and improving the overall development experience. By leveraging these functions appropriately, developers can write more readable, concise, and maintainable code in their Kotlin applications.

--

--

Rizwanul Haque

Lead Android Developer | Passionate about Coding | Sharing Insights 🚀 | Let's explore together! 📱💻 #AndroidDev #Kotlin #Tech