With Kotlin I wondered a lot about ‘with’

As I like to understand how things work and why they have been made the way they are, I wondered about the with method in Kotlin. But, before going further, for those who don’t know, with is often used to avoid repeating the same object when we want to use it over and over. For example, when you bind your ViewHolder in a RecyclerView, you often get something like this :

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
Person person = persons[position]
holder.tvTirstName.text = person.firstName
holder.tvLastName.text = person.lastName
holder.tvAge.text = person.age.toString()
Glide.with(context).load(person.avatarURL).into(holder.ivAvatar)
}

The same example using with would like this :

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
with(persons[position]) {
holder.tvTirstName.text = firstName
holder.tvLastName.text = lastName
holder.tvAge.text = age.toString()
Glide.with(context).load(avatarURL).into(holder.ivAvatar)
}
}

As you can see, this avoided us to repeat Person over and over again. Of course, moving the bind method in your ViewHolder would remove the necessity of holder. every time as well.

So you may be wondering what was so weird to me about with that I would write an article about it. To me, it was going against Kotlin principles. Indeed, with can receive a nullable and what was the point of having to surround my call with if (object != null) { with(object) {...} } when they could have done object?.with{...} . Then, when you look at the methods available, you can see there is already an extension function that does exactly this : run . So what is the point of with ?

To understand, I started by looking at its source code and most importantly the part highlighted in bold :

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

As an Android developer, one of the first thing I loved about Kotlin is the notion of nullable. But, in this code, they don’t seem to care if the receiver parameter is potentially null. So it raised another question to me, how does this actually compiles and work ? So i tried, what would happen if I compiled and run this code :

val test : String? = null
with(test) {
println(test.toString())
}

It actually compiles and prints null. I was surprised as I didn’t have to say test?.toString() and it did print the “value” of my variable. Did this mean that inside my with, it is not considered a nullable even though the value is null ? I was confused, so I tried something else :

val test : String? = null
with(test) {
println(this.length)
println(this.toString())
}

But this does not compile as test is still actually a nullable and we have to use the safe call operator ? to make the code compile :

val test : String? = null
with(test) {
println(this?.length)
println(this.toString())
}

So, what happens, how is it possible that block is called on receiver in the with method ? Why don’t we have to use the safe call operator for toString but we do for length ? The answer is actually in the documentation, something I would have known if I had read it all, which of course you did and that’s why you already know what is happening. It is called nullable receivers when creating an extension function on an object that is nullable, it is up to the method to handle the case where the object is null. That is why with lets you execute your code even if the value is null.

This is the case for the block received by with but also the case for toString() and that is why I could call this.toString() without the safe call operator !
The documentation shows us the code of toString() :

fun Any?.toString(): String {
if (this == null) return "null"
// after the null check, 'this' is autocast to a non-null type,
// so the toString() below
// resolves to the member function of the Any class
return toString()
}

There I had it! with lets you execute your code even if the value is null because, in some cases, that’s what you want. It will prevent you from repeating the variable name and make the code a little simpler to read. 
On the other hand, if you don’t want to execute the code when the value is null and have the same benefits of with, you should use ?.run .

One could argue that they could have not included with in Kotlin but it is true that, for most,with(object) {...} reads better. That is left to your appreciation :)