The Kotlin programming language, which was designed for a Java virtual machine (JVM), has a combination of both object-oriented and functional programming features, as well as other programming paradigms. Used in Android development, Kotlin provides a unique feature known as scope functions, however, many developers run into some difficulty when dealing with these functions.
As an Android mobile developer, it is important to have a full grasp of this concept, which is a crucial part of application development. The beauty of Kotlin comes from unique features that make it suitable for both frontend and backend development. In this tutorial, we’ll cover the following:
let
functionwith
functionrun
functionapply
functionalso
functionTo follow along with this tutorial, you’ll need the following:
Let’s get started!
In Kotlin, scope functions are used to execute a block of code within the scope of an object. Generally, you can use scope functions to wrap a variable or a set of logic and return an object literal as your result. Therefore, we can access these objects without their names. There are five types of scope functions in Kotlin: let
, with
, run
, apply
, and also
. Let’s consider these examples and their unique use cases.
There are many similarities between these five scope functions based on their similar operations, however, they differ in whether they return a lambda result or context object. They also vary in whether you refer to the context object using the this
or the it
keyword.
let
functionThe let
function has numerous applications, but it is generally used to prevent a NullPointerException
from occurring. The let
function returns the lambda result and the context object is the it
identifier. Let’s consider the following example:
fun main (){ val name: String? = null println(name!!.reversed) println(name.length) }
In the code snippet above, we assigned a null
value to the name
variable. We then printed the reverse
and the length
of the string by including a NotNull
assertion operator (!!)
to assert that the value is not null
because we have a nullable string name. Because we are calling the function on a null
value, this results in a NullPointerException
. However, we could prevent this by using the let
function with the following code:
fun main (){ val name: String? = null name?.let{ println(it.reversed) println(it.length) } }
We place our code inside the lambda expression of the let
function and replace the context object name with the it
identifier. To prevent the NullPointerException
, we include a safe call operator
, ( ?.)
, just after our name
object.
The safe call operator
places a condition and instructs our program to execute the code only if the name
object is NotNull
. In this example, we do not need to use the NotNull
assertion (!!)
.
Next, we’ll assign a string value “I love Kotlin”
to our name
variable. Then, we return this string value by saving our lambda value in a variable called lengthOfString
:
fun main (){ val name: String? = "I love Kotlin!!" val lengthOfString = name?.let{ println(it.reversed) println(it.length) } println(lengthOfString) }
with
functionThe with
function has a return type
as the lambda result, and the context object is the this
keyword, which refers to the object itself. Let’s consider the example in the code snippet below:
class Person{ var firstName: String = "Elena Wilson" var age: Int = 28 } fun main() { val person = Person() println(person.firstName) println(person.age) }
In the code snippet above, we created a Person
class and assigned some properties, firstName
and age
. Next, in our main function, we printed out the values using println
, which is used for cli
output.
Let’s imagine that we had over twenty properties in the Person
class, which would result in multiple code repetitions. We can correct this by using the with
function and passing the person
object in the lambda expression using the this
keyword:
n){ println(this.firstName) println(this.age) }
The context object here refers to the person
object on which the operation is performed. The return value of the with
function is a lambda result. Imagine we decide to add ten years to the age
and store the value in a variable called personInfo
, which is of the type integer
:
val person = Person() val personInfo : String = with (person){ println(this.firstName) println(this.age) age + 10 "I love the game of football" } println(personInfo) }
The value produced is “I love the game of football”
. In summary, the with
function returns a lambda function and uses the this
keyword as the context object.
run
functionThe run
function returns the lambda result, and we refer to the context object by using the this
keyword. The run
function is a combination of the with
and let
functions. Let’s consider the example in the code snippet below:
fun main { val person: Person? = Person() val bio = person?.run { println(name) println(age) "LogRocket rocks!!!" } println(bio) }
Assuming we decide to assign a null value to the person
object, we’d have to prevent a NullPointerException
from occurring. We can achieve this by calling the run
function with the person
object. Next, we’ll return the lambda function bio
.
apply
functionapply
is a higher order function. The apply
function returns a context object, and the context object returns this
. Let’s consider the following example:
val car = Car() var carName: String = "" var carColor: String = "" fun main { val car = Car().apply { carName = "Lamborghini" carColor = "Navy blue" } } with(car){ println(carName) println(carColor) }
also
functionThe also
function is similar to the previous functions in that it is used to perform an operation on a particular object after it has been initialized. The also
function returns the context object, and the context object can be referred to using the it
identifier. Let’s refer to the code snippet below for further detail:
fun main(){ val numberList: mutableList<Int> = mutableListOf(1,2,4,5) numberList.also{ println("The list of items are: $numberList") numberList.add(6) println("The list of items after adding an element are: $numberList") numberList.remove(4) println("The list of items after removing an element are: $numberList") } }
From the code above, we created a numbersList
object with five integer values and performed some operations under the numbersList
object. We then utilized the also
function. Note that in the also
function, we can refer to the numberList
by using the it
identifier, as seen in the code snippet below:
fun main(){ val numberList: mutableList<Int> = mutableListOf(1,2,4,5) val multipleNumbers = numberList.also { println("The list of items are: $it") it.add(6) println("The list of items after adding an element are: $it") it.remove(4) println("The list of items after removing an element are: $it") } println("The original numbers are: $numberList") println("The multipleNumbers are: $multipleNumbers) }
Another way to implement the also
function is using the it
and also
keywords like in the code snippet below. We use the also
function to modify the value of the firstName
variable by assigning Eden Peter
to it:
fun main { val person = Person().apply { firstName = "Eden Elenwoke" age = 22 } with(person){ println(firstName) println(age) } person.also{ it.firstName = "Eden Peter" println("My new name is: + ${it.firstName}") } }
Using scope functions in the right place might seem a bit tricky at first, but it largely depends on what we want to achieve with project. Let’s refer the summary below as a guide to inform us on which scope function to use for each unique use case:
apply
: You want to configure or initialize an objectwith
: You want to operate on a non-null objectlet
: You want to execute a lambda function on a nullable object and avoid NullPointException
run
: You want to operate on a nullable object, execute a lambda expression, and avoid NullPointerException
. This is the combination of the with
and let
function featuresalso
: You want to perform some additional object operations and configurationsLet’s compare a scope function and a normal function with a few examples. Let’s consider a normal function using a class
named Student
with three attributes, studentName
, studentNumber
, and studentAge
, like below:
Class Student { var studentName : String? = null var studentNumber : String? = null var studentAge : Int? = null }
With the code snippet below, we instantiate our class and assign values to it:
val student = Student () student.studentName = "Peter Aideloje" student.studentNumber = 08012345678 student.studentAge = 28
Using a scope function
can help us to achieve the same results as above in a simpler and cleaner way with less code. Let’s compare our expression above with a scope
function in the code snippet below:
val person = Student().apply{ studentName = "Peter Aideloje" studentNumber = 08012345678 studentAge = 28 }
In the code snippet above, we instantiate the Student
object and call the apply
function. Then, we assign the studentName
, studentNumber
, and studentAge
properties within the lambda expression.
When we compare the scope function and the normal function in the examples above, we notice that we successfully eliminated code repetition where the student
object name was repeated multiple times. Using a scope function makes our code more concise and readable, and we initialized our properties without using the student
object name.
From the examples in the function comparison section above, we have come to realize some benefits of using scope functions:
For further reading, you can also checkout the official Kotlin documentation.
In this article, we introduced the five scope functions in Kotlin. We also considered some unique use cases with examples, reviewing when to use each scope function. We compared scope functions with normal functions and finally reviewed the benefits of using scope functions.
As Android development continues to grow in popularity with more Android devices on the market, knowledge of the Kotlin programming language will become more crucial. I hope this article was helpful, and please feel free to leave a comment if you have any questions. Happy coding!
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.