Kotlin Scope Functions Best Practices with 10 Coding Challenges

Kotlin Scope Functions

What is a scope function in Kotlin?

In Kotlin, scope functions are a set of functions that allow concise and expressive ways to operate on objects within a certain scope. There are five main scope functions in Kotlin: let, run, with, apply, and also. Each of these functions has its own unique characteristics, but they all help in simplifying and organizing code

What are the Kotlin Scope Functions?

There are five main scope functions in Kotlin: let, run, with, apply, and also.

Choosing the right scope function in Kotlin depends on the specific use case and the goals you want to achieve. Each scope function has its own characteristics and use cases, so understanding their differences will help you decide which one to use in a particular scenario.

Kotlin Scope Functions

Use let when:

You want to perform operations on a non-null object.You need to transform the object and return a result.You want to introduce a new variable scope.

val originalString: String? = "hello"

val result = originalString?.let {
    // Operations on non-null object
    it.length
}

println(result)
Kotlin

Explanation:

  • originalString?.let { ... } checks if originalString is non-null.
  • If it’s non-null, the block inside let is executed, and the result is the length of the string.
  • The result is printed, and if originalString is null, nothing happens.

Use run when:

You want to execute operations on the object itself.You need to perform multiple operations and return a result.You want to isolate a block of code.

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

val person = Person("John", 25)

val result = person.run {
    // Operations on the object
    age += 5
    "$name is now $age years old"
}

println(result)
Kotlin

Explanation:

  • person.run { ... } executes operations on the person object.
  • It increments the age by 5 and creates a string with the updated information.
  • The result is printed.

Use with when:

You want to execute multiple operations on an object, but without changing the object itself.You prefer a function-style call for improved readability.

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

val myCar = Car("Toyota", "Camry", 2022)

val result = with(myCar) {
    // Operations on the object
    model = "Corolla"
    year += 1
    "My car is a $brand $model from $year"
}

println(result)
Kotlin

Explanation:

  • with(myCar) { ... } allows multiple operations on myCar.
  • It updates the model and increments the year.
  • The result is a string describing the modified car.

Use apply when:

You want to initialize or configure properties of an object.You want to make changes to the object and return the modified object itself.

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

val myBook = Book("The Kotlin Guide", "John Doe", 200)

val modifiedBook = myBook.apply {
    // Initialize or configure properties
    title = "Mastering Kotlin"
    pages = 300
}

println("Original Book: $myBook")
println("Modified Book: $modifiedBook")
Kotlin
  • myBook.apply { ... } initializes or configures properties of myBook.
  • It creates a new object (modifiedBook) with the changes.
  • The original and modified books are printed.

Use also when:

You want to perform additional actions on an object.You want to inspect or log information about the object without modifying it.You want to use it in a chain without affecting the final result.

val numbers = mutableListOf(1, 2, 3, 4)

val result = numbers.also {
    // Additional actions on the object
    it.add(5)
    it.remove(2)
}.joinToString()

println("Modified List: $result")
Kotlin

Explanation:

  • numbers.also { ... } performs additional actions on the numbers list.
  • It adds 5 and removes 2 from the list.
  • The modified list is printed using joinToString().

What is the difference between let and run Kotlin Scope Functions?

let is mainly used for operations on the result of a function or property, while run is used for scoping and executing operations on the object itself.

Which Kotlin Scope Functions accepts argument?

Among the Kotlin scope functions (let, run, with, apply, and also), let is the one that explicitly takes the result of the expression it operates on as an argument.

Here’s an example:

val result = "Hello, World!".let { stringValue ->
    println(stringValue)
    stringValue.length
}
Kotlin

In this example, the lambda expression inside let receives the value of “Hello, World!” as stringValue, and you can perform operations on it within the lambda.

The other scope functions (run, with, apply, and also) don’t explicitly pass the result as an argument; they operate on the context implicitly.

Note:

  • it is used within let and also to refer to the object being processed.
  • this is used within run, with, and apply to refer to the receiver object.
Kotlin Scope Functions

Let’s explore use cases for each scope function in the context of Android development.

1. let – Null Check and Transformation:

Use Case: In Android, you often deal with nullable values, especially when working with views. Let’s say you have an EditText where a user enters their name.

val userInput: String? = editText.text.toString()

val formattedName = userInput?.let {
    // Perform operations on non-null input
    it.trim().capitalize()
} ?: "Default User"

textView.text = "Hello, $formattedName!"
Kotlin

Explanation:

  • userInput?.let { ... } checks if userInput is non-null.
  • If non-null, it trims and capitalizes the input, providing a formatted name.
  • If null, it uses a default value.
  • The formatted name is then displayed in a TextView

2. run – Initializing Views in an Activity:

Use Case: When initializing UI components in an Android Activity, you might use run to configure the properties of views.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val resultTextView = run {
            val textView = TextView(this)
            textView.textSize = 18f
            textView.setTextColor(ContextCompat.getColor(this, android.R.color.black))
            textView
        }

        // Now, use resultTextView in your layout.
        // (e.g., add it to a LinearLayout or set it as the text view of a button)
    }
}
Kotlin

Explanation:

  • run { ... } initializes and configures a TextView.
  • The properties like text size and text color are set.
  • The resulting TextView is stored in resultTextView and can be used in the layout.

3. also – Logging and Side Effects:

Use Case: During development, you might use also for logging or performing additional side effects.

class MyViewModel : ViewModel() {
    private val repository = MyRepository()

    fun fetchData() {
        repository.fetchData()
            .also { result ->
                // Log the result for debugging
                Log.d("MyViewModel", "Data fetched: $result")
            }
            .observeForever { data ->
                // Update UI or perform other actions with the fetched data
            }
    }
}
Kotlin
  • repository.fetchData().also { ... } logs the result of the data fetch operation.
  • The fetched data is observed, and UI updates or other actions can be performed based on the result.

“side effect” refers to any observable change that is not directly related to the return value of a function or expression. It’s an external impact or modification that occurs during the execution of code, influencing the state of the system.

When you write code, you often have a main goal, like performing a calculation or updating data. However, there can be additional actions or changes that happen as a result of your code, which may not be directly related to the main goal.

For example, if you’re writing code to update a user’s profile information, a side effect could be logging information for debugging purposes or sending a notification. These actions aren’t the primary focus of the code, but they happen as a consequence of the main operation.

4. with – Customizing AlertDialog:

Use Case: When creating a custom AlertDialog in Android, with can be used to configure various aspects of the dialog.

class MyFragment : Fragment() {

    fun showCustomDialog() {
        with(AlertDialog.Builder(requireContext())) {
            setTitle("Confirmation")
            setMessage("Do you want to proceed?")
            setPositiveButton("Yes") { _, _ ->
                // Handle positive button click
            }
            setNegativeButton("No") { _, _ ->
                // Handle negative button click
            }
            create().show()
        }
    }
}
Kotlin
  • with(AlertDialog.Builder(requireContext())) { ... } configures the properties of the AlertDialog.
  • It sets the title, message, and positive/negative button click listeners.
  • The dialog is then created and shown.

5. also – Logging and Modifying State:

Use Case: When processing user input in an Android app, also can be used for logging and modifying the state.

class MyActivity : AppCompatActivity() {

    fun processUserInput(input: String) {
        input.also {
            // Log the original input
            Log.d("MyActivity", "Original input: $it")
        }.let { trimmedInput ->
            // Further operations on the trimmed input
            val processedInput = trimmedInput.toUpperCase()
            // Use the processed input in your app...
        }
    }
}
Kotlin

Explanation:

  • input.also { ... } logs the original input.
  • The result of input.also (original input) is passed to let for further processing.
  • In this example, the input is trimmed and converted to uppercase for further use.

Kotlin Scope Functions Coding Challenges

Given a nullable string, transform it to uppercase if it’s not null, and return a default string otherwise.

Solution using let:

fun transformString(input: String?): String {
    return input?.let {
        it.toUpperCase()
    } ?: "Default String"
}
Kotlin

Challenge: Create a function that configures and returns a Person object with a default name and age.

Solution using apply

data class Person(var name: String = "John", var age: Int = 30)

fun createDefaultPerson(): Person {
    return Person().apply {
        name = "Default Name"
        age = 25
    }
}
Kotlin

Challenge: Given a list of numbers, log each number and calculate the sum of the list.

Solution using also and run:

fun processNumbers(numbers: List<Int>): Int {
    val sum = numbers.also {
        it.forEach { number ->
            // Log each number
            println("Processing number: $number")
        }
    }.run {
        // Calculate and return the sum
        this.sum()
    }

    return sum
}
Kotlin
  1. numbers: List<Int>: The bag is called numbers, and it contains a bunch of numbers.
  2. numbers.also { ... }: This part is like opening the bag (numbers) and looking at each number one by one. The special thing you want to do with each number is written inside the curly braces ({ ... }).
    • it.forEach { number ->: This is like saying, “For each number in the bag, do the following…”
    • println("Processing number: $number"): The special thing is to tell everyone about each number by printing a message. It’s like saying, “I’m processing this number: [number].”
  3. .run { ... }: After you’ve looked at each number and told everyone about it, now you want to do something with the whole bag. In this case, you want to calculate the sum of all the numbers.
    • this.sum(): This is like adding up all the numbers in the bag to find the total sum.
  4. val sum = ...: The result of all this is a total sum, and you put it in a special box (variable) called sum.
  5. return sum: Finally, you say, “Here is the total sum after processing all the numbers!”

Challenge: Given a nullable Person object, perform a series of operations if it’s not null, and return a default Person otherwise.

Solution using run and apply:

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

fun processPerson(input: Person?): Person {
    return input?.run {
        // Perform operations on non-null Person
        apply {
            name = name?.toUpperCase()
            age = age?.plus(5)
        }
    } ?: Person("Default Name", 25)
}
Kotlin

Challenge: Given a list of numbers, filter out the even numbers and double the remaining ones.

Solution using let and map:

fun filterAndDouble(numbers: List<Int>): List<Int> {
    return numbers.let { list ->
        list.filter { it % 2 != 0 }.map { it * 2 }
    }
}
Kotlin

Explanation:

  • numbers.let { list ->: Start a scope using let and give the list a name (list).
  • list.filter { it % 2 != 0 }: Filter out the even numbers from the list.
  • list.map { it * 2 }: Double each remaining number in the filtered list.
  • The final result is a list of doubled odd numbers.

Challenge: Create a function to customize and return an AlertDialog with a title and message.

Solution using with:

fun createCustomAlertDialog(context: Context): AlertDialog {
    return with(AlertDialog.Builder(context)) {
        setTitle("Important Message")
        setMessage("This is a custom alert dialog.")
        setPositiveButton("OK") { _, _ ->
            // Handle positive button click
        }
        setNegativeButton("Cancel") { _, _ ->
            // Handle negative button click
        }
        create()
    }
}
Kotlin

Explanation:

  • with(AlertDialog.Builder(context)) {: Start a scope using with and create an AlertDialog.Builder.
  • setTitle("Important Message"): Set the title of the alert dialog.
  • setMessage("This is a custom alert dialog."): Set the message of the alert dialog.
  • setPositiveButton("OK") { _, _ ->: Set a positive button with a click listener.
  • setNegativeButton("Cancel") { _, _ ->: Set a negative button with a click listener.
  • create(): Create and return the customized AlertDialog

Challenge: Given a string, count the occurrences of each character and return a map.

Solution using run and groupBy:

fun countCharacters(input: String): Map<Char, Int> {
    return input.run {
        groupBy { it }
            .mapValues { it.value.size }
    }
}
Kotlin

Explanation:

  • input.run {: Start a scope using run with the string as the receiver.
  • groupBy { it }: Group the characters in the string based on their values.
  • mapValues { it.value.size }: For each group, map the character to its count.
  • The final result is a map representing the count of each character.

Challenge: Given a list of names, extract the first names and capitalize them.

Solution using let and map:

fun extractAndCapitalizeNames(names: List<String>): List<String> {
    return names.let { list ->
        list.map { it.substringBefore(" ").capitalize() }
    }
}
Kotlin
  • names.let {: Start a scope using let with the list of names as the receiver.
  • list.map { it.substringBefore(" ").capitalize() }: For each name, extract the first name and capitalize it.
  • The final result is a list of capitalized first names.

Challenge: Write a function to calculate the factorial of a given non-negative integer.

Solution using run and recursion:

fun calculateFactorial(n: Int): Long {
    return n.run {
        if (this <= 1) 1L else this * calculateFactorial(this - 1)
    }
}
Kotlin

Explanation:

  • n.run {: Start a scope using run with the integer as the receiver.
  • if (this <= 1) 1L else this * calculateFactorial(this - 1): Perform a recursive factorial calculation.
  • The final result is the factorial of the given integer.

Challenge: Given a list of numbers, filter out the odd numbers and calculate their sum.

Solution using let and filter:

fun sumOfFilteredOddNumbers(numbers: List<Int>): Int {
    return numbers.let { list ->
        list.filter { it % 2 != 0 }
            .sum()
    }
}
Kotlin

Explanation:

  • numbers.let {: Start a scope using let with the list of numbers as the receiver.
  • list.filter { it % 2 != 0 }: Filter out the odd numbers from the list.
  • sum(): Calculate and return the sum of the filtered odd numbers.
  • The final result is the sum of odd numbers.

Question 1: Which Kotlin scope function is used when you want to perform operations on a non-null object and return a result?

a) run
b) let
c) with
d) apply

Explanation: b) let
In Kotlin, let is used when you want to perform operations on a non-null object and return a result. It provides a concise way to transform or process an object and return the result of the lambda expression.


Question 2: In which Kotlin scope function do you often see the usage of it as the default name for the lambda parameter?

a) run
b) let
c) also
d) apply

Explanation: b) let
The it keyword is often used as the default name for the lambda parameter in the let scope function. It refers to the object on which the function is invoked.


Question 3: When using the apply scope function, what does it return?

a) Modified object
b) Result of the last operation
c) Original object
d) Result of the lambda expression

Explanation: c) Original object
The apply scope function returns the original object on which it is applied. It is commonly used for configuring or initializing properties of an object.


Question 4: Which scope function is often used for initializing properties of an object?

a) run
b) let
c) with
d) also

Explanation: d) also
The also scope function is often used for initializing properties of an object. It allows you to perform additional actions on the object and returns the original object.


Question 5: In Kotlin, which scope function is suitable for performing additional actions on an object without changing it?

a) let
b) also
c) run
d) apply

Explanation: b) also
The also scope function is suitable for performing additional actions on an object without changing it. It returns the original object and is often used for side effects.


Question 6: Which scope function is commonly used for chaining operations on a nullable object?

a) let
b) also
c) run
d) with

Explanation: a) let
The let scope function is commonly used for chaining operations on a nullable object. It allows you to execute a block of code if the object is not null.


Question 7: In Kotlin, what does this refer to when used within a scope function?

a) Result of the lambda expression
b) The object itself
c) Modified object
d) Default name for the lambda parameter

Explanation: b) The object itself
In Kotlin scope functions, this refers to the receiver object on which the function is applied. It is used within the context of the current object.


Question 8: Which scope function is often used for executing operations on an object itself without the need to reference the object explicitly in each line of code?

a) let
b) also
c) run
d) with

Explanation: c) run
The run scope function is often used for executing operations on an object itself without the need to reference the object explicitly in each line of code.


Question 9: When using also in combination with another scope function, what does also return?

a) Modified object
b) Result of the last operation
c) Original object
d) Result of the lambda expression

Explanation: c) Original object
The also scope function returns the original object. It is often used in combination with other scope functions to perform additional actions without changing the object.


Question 10: Which scope function is suitable for scenarios where you want to apply multiple changes to the same object?

a) run
b) let
c) with
d) apply

Explanation: d) apply
The apply scope function is suitable for scenarios where you want to apply multiple changes to the same object. It returns the modified object and is often used for configuration.

In Kotlin, which scope function is used when you want to perform operations on an object and ignore the return value?

a) run
b) let
c) also
d) apply

Explanation: c) also
The also scope function is used when you want to perform additional actions on an object without changing it and ignore the return value. It is often used for side effects.


Question 12: When using the with scope function, what is the receiver object implicitly?

a) this
b) it
c) receiver
d) obj

Explanation: a) this
When using the with scope function, the receiver object is implicitly referenced by this. It is a concise way to perform operations on an object without using the object reference explicitly.


Question 13: Which scope function is commonly used in extension functions?

a) run
b) let
c) also
d) apply

Explanation: a) run
The run scope function is commonly used in extension functions. It allows you to execute a block of code on the receiver object and implicitly references the object as this.


Question 14: What does the it keyword refer to in the context of the let scope function?

a) Result of the lambda expression
b) The object itself
c) Modified object
d) Default name for the lambda parameter

Explanation: b) The object itself
In the context of the let scope function, the it keyword refers to the object on which the function is invoked. It is the default name for the lambda parameter.


Question 15: Which scope function is often used for conditional execution based on the object’s state?

a) run
b) let
c) also
d) apply

Explanation: b) let
The let scope function is often used for conditional execution based on the object’s state. It allows you to execute a block of code only if the object is not null.

Kotlin Functions

Advanced Functions Higher-Order Functions – Lambda Functions – Extension Functions – Infix Functions- Generic Functions
See More Info

Functions– Simple Functions – Function Parameters – Function Return Types – Function Scope
See More Info

Functional Programming FunctionsImmutability and Pure Functions- Function Composition – Functional Operators
See More Info

Feel free to follow and join my email list at no cost. Don’t miss out — sign up now!

Please check out my earlier blog post for more insights. Happy reading!

The Innovative Fundamentals of Android App Development in 2023

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x