Mastering Kotlin's Scope Functions: A Comprehensive Guide to let, apply, run, and with.
Hello Devs,
Today we are going to learn how to write more readable and concise code by using scope functions in Kotlin. If you have vast experience with Kotlin or have developed a number of Android applications with Kotlin then I bet you have used scope functions somewhere in your code but do you really know the differences between the 5 types of scope functions let
, apply
, run,
with
and also
or in which use cases one is supposed to be used. By the end of this article, you will have a clear understanding of where to use each one of them.
Let's jump in...
What are scope functions?
Scope functions are functions that execute a block of code within the context of an object and return either a result or an object depending on the scope function used.
A result can be returned from a computation performed and an object can be returned from a modification or configuration of an object again this will depend on the scope function used, by now you're starting to see the difference and application between each one of the scope functions.
To get a proper hang of it let's jump more into the details and look at each one of the scope functions with a few lines of code.
Types of Scope Functions
There are 5 types of scope functions in Kotlin.
let
apply
run
with
also
Scope Functions: Practical Examples and Use Cases
Let's look at practical examples of the usage of scope functions using a few lines of code to get a good understanding of the functions and be able to distinguish between each one of them.
let
let
operator takes an object and returns the result of the lambda expression and refers to the context of an on object as it.
let
can also be used as a null safety operator since it only executes on in null objects and skips execution if an object is null. Let's look at an example
private fun performRunOperation() {
val car: Car? = null
car?.let {
it.model = "Toyota"
it.registrationNumber = "0987654321"
print("The details of the Person of null object is:" + it.displayCarDetails())
}
Car().let {
it.model = "Toyota"
it.registrationNumber = "0987654321"
print("The car dtails of non null object is :" + it.displayInfo())
return@let "The car details of non null object is :" + it.displayInfo()
}
}
//Output :
// The car details of non null object is :Model: Toyota, registrationNumber: 0987654321, OwnerID : 01123456
The first execution is skipped since the car is null and let's
ensure its execution is only performed on nonnull values.
From this, we have learned the following about let
the operator
Executes nonnull values only and skips execution if the value is null.
Uses
it
for object reference.Returns result of lambda expression.
Accepts return statements.
also
also
operator as the name suggests is used to perform additional operations on already initialised objects within a chain of operations which can be used to perform side effects and also uses it
is for object reference.
It's easy to confuse let
operator and also
operator but the only similarity they have is the way they use it
as a reference to the context and the difference is that let's
accept a return statement while also
does not accept a return statement.
Let's look at an example:
data class Car(
var model: String,
)
private fun performRunOperation() {
val carList = listOf(
Car("Subaru"),
Car("Mercedes"),
Car("Volvo"),
Car("Toyota"),
Car("Nissan")
)
var cars: List<Car> = carList
val result = cars
.map { it.model.toUpperCase() }
.also { println("After Upppercase conversion: $it") }
.filter { it.length > 4 }
.also { println("After filtering: $it") }
.joinToString(separator = ", ")
println("Final result: $result")
}
fun main(args: Array<String>) {
performRunOperation()
}
// After converting to uppercase: [SUBARU, MERCEDES, VOLVO, TOYOTA, NISSAN]
// After filtering: [SUBARU, MERCEDES, VOLVO, TOYOTA, NISSAN]
// Final result: VOLVO
To understand this, let's analyze the performRunOperationFunction
we have a list of Car objects in which we are going to perform a chain of operations and this is where also,
the operator comes in handy and here we use also
as a logger to print the results after each step which would have been harder without it first, we map our object to uppercase then print them filter the length and print the one whose length is below 4 and print and the chaining can go on and on.
apply
apply
is used to reference objects or runs on the receiver and this
is to access the objects as opposed to also
and let
which use it
and does not accept a return statement and requires the object reference to be run in an expression.
Let's look at an example
data class Car(
var model: String,
var owner:String,
var registration:String
)
private fun performRunOperation() {
val car = Car("Subaru","Peter","KBJ 456Y")
car.apply {
this.model = "Toyota"
this.owner = "Mike"
}
print(car)
}
fun main(args: Array<String>) {
performRunOperation()
}
//Car(model=Toyota, owner=Mike, registration=KBJ 456Y)
We have returned the same car object but modified it within the car scope and can be useful and come in handy when you want to make multiple changes or modifications to an object during its creation. It does this in a clean and concise way since it provides a convenient and easy way to initialize, modify or configure objects.
The use of this
is totally optional, the code can do without.
run
run
operator can be used to execute non-null blocks of code and hence can perform null safety calls. It's like a combination of let
and with
.
Uses this
to refer to its objects.
Let's look at an example
data class Car(
var model: String? = "Toyota",
var owner:String? = "Mike",
var registration:String = "KBGL5674W"
)
private fun performRunOperation() {
// Since car is initialised to null the run operator
// block wont be executed and a non exception wont occur
var car:Car? = null
car?.run {
print(model)
}
// This will be executed since car is not null
val cars:Car = Car()
cars.run {
print(model)
}
}
fun main(args: Array<String>) {
performRunOperation()
}
//Toyota
Here we have tested therun
operator on a null object to perform a null safety call, but since the car object is null, the block of code won't be executed, but when we call a non-null object, it will be safely executed. Also, we are usingthis
to reference the context object as a lambda receiver, so we did not use this to access the member receivers.
with
with
is similar to using run
and both use this
to refer to their objects and also return lambda result and so what's the difference?
We can perform null checks using the with
the operator however, there will be a slight difference in syntax since we cannot use the safe call operator (?) on it we will have to use a longer route which might not be convenient so you can use with
non-null receivers and run
for null receivers.
Summary
Object reference :
this
orit
Return values: Either lambda or context value itself.
apply
andalso
return the context object.let
,run
andwith
return the lambda resultlet
andalso
usesit
is for object referencerun
,with
andapply
usethis
is for object reference.
Happy coding :)