1. Overview
with() is one of the scope functions in Kotlin.
In this tutorial, we’ll take a closer look at the with() function.
2. Introduction to the with() Function
As a member of Kotlin’s scope function, the with() function references a context object and executes a block of code within that context.
To better understand the usage of the with() function, let’s first have a look at how it’s defined:
public inline fun <T, R> with(receiver: T, block: T.() -> R): R
Let’s break it down and understand it:
- the first argument – receiver in type T, the so-called context object
- the second argument – block, which is a lambda expression, contains the logic we want to execute within the context above
- the with() function returns the lambda expression’s return value
Next, let’s learn how to use the with() function through examples.
For simplicity, we’ll use unit test assertions to verify whether the returned value from with() is expected.
3. The Usage of the with() Function
Next, let’s understand how to use the with() function by an example. Let’s say we have a data class Player:
data class Player(val firstname: String, val lastname: String, val totalPlayed: Int, val numOfWin: Int)
We’d like to get a description string of a Player instance in this format: “firstname lastname’s win-rate is rate%”.
So next, let’s write a test using with():
val tomHanks = Player(firstname = "Tom", lastname = "Hanks", totalPlayed = 100, numOfWin = 77)
val expectedDescription = "Tom Hanks's win-rate is 77%"
val result = with(tomHanks) {
"$firstname $lastname's win-rate is ${numOfWin * 100 / totalPlayed}%"
}
assertEquals(expectedDescription, result)
As the code above shows, the tomHanks object is the context object of the with() function. In the with{…} block, the code is within tomHanks‘s context. Therefore, we can directly access properties of the context object, such as “*$firstname $lastname*“. Here, we’ve used Kotlin’s string template.
If we run the test, it passes. So, with{ … } returns the lambda result. In this case, it’s the description string.
When we want to reference the context object in the with{…} block, we can use the this keyword:
val tomHanks = Player(firstname = "Tom", lastname = "Hanks", totalPlayed = 100, numOfWin = 77)
val result = with(tomHanks) {
"$this"
}
assertEquals("$tomHanks", result)
In the test above, we return tomHanks‘s toString() result in the with{…} block. Again, we’ve used a string template. Here, $this* is the same as *this.toString().
Therefore, when we use with(), we should keep in mind a couple of things:
- All code in the with{…} block is within the receiver object’s context. If we want to reference that object, we use this
- with{…} returns the lambda result
Some scope functions reference the receiver object using it, for example, let() and also(). However, some scope functions use this, which is the same as with(), like apply() and run(). Next, let’s compare them to see the difference between apply()/run() and with().
4. with() vs apply()
Both with() and apply() reference the context object using the this keyword. The significant difference between with() and apply() is with() returns the lambda result. On the other hand, apply() returns the receiver object. Therefore, apply() is usually used for object configuration.
For example, let’s say we have a class Player2:
class Player2(val id: Int) {
var firstname: String = ""
var lastname: String = ""
var totalPlayed: Int = 0
var numOfWin: Int = 0
// hashcode and equals() methods omitted.
// Both methods check all properties in the class
}
Now, let’s see how apply() is used for object configuration:
val tomHanks = Player2(7)
tomHanks.firstname = "Tom"
tomHanks.lastname = "Hanks"
tomHanks.totalPlayed = 100
tomHanks.numOfWin = 77
val result = Player2(7).apply {
firstname = "Tom"
lastname = "Hanks"
totalPlayed = 100
numOfWin = 77
}
assertEquals(tomHanks, result)
As we can see, the apply() function returns the context object itself. In this example, it’s Player2(7). In the apply{…} block, we set different properties of the receiver object.
The test passes if we give it a run. However, if we change apply() to with() in the example above, the result is not a Player2 instance. Instead, it will be in Unit type (similar to Java’s void) as the lambda doesn’t return any value:
val result = with(Player2(7)) {
firstname = "Tom"
lastname = "Hanks"
totalPlayed = 100
numOfWin = 77
}
assertTrue { result is Unit }
Sharp eyes may have noticed the ways to call with() and apply() are also different:
- someObject.apply { … }
- with(someObject) { … }
This is because apply() is an extension function of all types, but with() isn’t:
public inline fun <T> T.apply(block: T.() -> Unit): T {..}
5. with() vs run()
The run() function is another scope function using this to reference the context object. Further, like with(), the run() function returns the lambda result too.
The only difference between run() and with() is that run() is an extension function of all types:
public inline fun <T, R> T.run(block: T.() -> R): R = block()
Therefore, we can use run() in this way: someObject.run { … }. So, we can see that with() and run() are pretty similar except for invocations.
However, if the receiver object is nullable, the run approach is easier to read. Let’s see an example:
fun giveMeAPlayer(): Player? {
return Player(firstname = "Tom", lastname = "Hanks", totalPlayed = 100, numOfWin = 77)
}
As the above code shows, we’ve created a function giveMeAPlayer() to return a nullable Player instance. Of course, for simplicity, we let the function always return the same object.
Next, let’s have a look at how with() and run() handle nullable objects:
val expectedDescription = "Tom Hanks's win-rate is 77%"
// using run
val runResult = giveMeAPlayer()?.run { "$firstname $lastname's win-rate is ${numOfWin * 100 / totalPlayed}%" }
assertEquals(expectedDescription, runResult)
// using with
val withResult = with(giveMeAPlayer()) {
if (this != null) "$firstname $lastname's win-rate is ${numOfWin * 100 / totalPlayed}%" else null
}
assertEquals(expectedDescription, withResult)
We can see in the test, since run() is an extension function, we can use the null-safe call someNullable?.run { … } to make the run block only execute when someNullable is not null. However, on the with() side, we cannot use the null-safe feature. So we need to do a null check in the block.
If we run the test, it passes. So, both approaches give the same result.
6. Conclusion
In this article, we’ve learned how to use the with() function. Further, we’ve discussed the differences between with(), run(), and apply().
As always, the full source code used in the article can be found over on GitHub.