DEV Community

Manh-Ha VU
Manh-Ha VU

Posted on • Originally published at manhhavu.com

Using Kotlin scope functions to create deeply nested Java objects easily

In some cases, especially when working with an API with a provided Java SDK, we often find ourselves in creating a (very) deeply nested Java object using setters. Creating Java object with a lot of setters is annoying and error-prone. Kotlin scope function can help to make our code much more readable in these cases.

Problem

Here is a short example of object creation using setters to understand the problem. A more complex example can be found below.

val name = PersonalName()
name.firstName = "Wile"
name.surname = "Coyote"
name.surnamePrefix = "E."
name.title = "Mr."

val personalInformation = PersonalInformation()
personalInformation.dateOfBirth = "19490917"
personalInformation.gender = "male"
personalInformation.name = name

In this example, we have to introduce unnecessary variables to set the child objects, which might easily introduce a bug if forgetting to link mother and child objects.

If Address and PersonalName are data classes, things might get easier:

PersonalInformation(
    dateOfBirth = "19490917"
    gender = "male"
    name = PersonalName(
        firstName = "Wile"
        surname = "Coyote"
        surnamePrefix = "E."
        title = "Mr."
    )
)

Unfortunately, these classes are part of an external library, we should not, of course, waste our time rewriting them. But, can we do it better? Yes, we can use the excellent scope function apply() for this job.

Refactor using Kotlin scope function

Before giving more explanation in the next section, we will use directly the apply() function to demonstrate the power of the scope function. The apply function is an extension function that is available on any class.

// PersonalName can be rewritten as
val name = PersonalName().apply {
    firstName = "Wile"
    surname = "Coyote"
    surnamePrefix = "E."
    title = "Mr."
}

// And PersonalInformation can be rewritten
val personalInformation = PersonalInformation().apply {
    dateOfBirth = "19490917"
    gender = "male"
    name = name
}

or without the intermediary variable name:

PersonalInformation().apply {
    dateOfBirth = "19490917"
    gender = "male"
    name = PersonalName().apply {
      firstName = "Wile"
      surname = "Coyote"
      surnamePrefix = "E."
      title = "Mr."
  }
}

We can see that this version is not far from the data class version. A more complex example can be found below.

How does apply() work?

apply() function is actually pretty simple. It has following (simplified) signature:

fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

If you aren't familiar with T.() -> Unit, technically it is a function literal with receiver. In other words, inside the block() function, we have access to the T object via this reference. Using the same example above:

val name = PersonalName().apply {
    firstName = "Wile"
    surname = "Coyote"
    surnamePrefix = "E."
    title = "Mr."
}

// is actually equivalent to this more verbose code
val name = PersonalName().apply {
    this.firstName = "Wile"
    this.surname = "Coyote"
    this.surnamePrefix = "E."
    this.title = "Mr."   
}

In the second code block, this = current instance of PersonalName = T in the apply() signature. In a Kotlin/Java class, most of the time, when calling class' method/property, you don't need to specify this reference. This is the same idea as in the first code block.

Without the receiver syntax, meaning:

fun <T> T.apply(block: (T) -> Unit): T

we'd have to write by specifying it reference in the block(). The code is a little longer and less natural. In this case, it = current instance of PersonalName

val name = PersonalName().apply {
    it.firstName = "Wile"
    it.surname = "Coyote"
    it.surnamePrefix = "E."
    it.title = "Mr."
}

Apply() is not the only scope function, there are more other functions like also, let, use, with, ... which are very well explained in the Kotlin reference. We might talk more about them in a later post.

More complex example

All examples of this post are borrowed from Ingenico API documentation.

Before using apply()

val billingAddress = Address()
billingAddress.additionalInfo = "b"
billingAddress.city = "Monument Valley"
billingAddress.countryCode = "US"
billingAddress.houseNumber = "13"
billingAddress.state = "Utah"
billingAddress.street = "Desertroad"
billingAddress.zip = "84536"

val name = PersonalName()
name.firstName = "Wile"
name.surname = "Coyote"
name.surnamePrefix = "E."
name.title = "Mr."

val personalInformation = PersonalInformation()
personalInformation.dateOfBirth = "19490917"
personalInformation.gender = "male"
personalInformation.name = name

val customer = Customer()
customer.accountType = "none"
customer.billingAddress = billingAddress
customer.locale = "en_US"
customer.merchantCustomerId = "1234"
customer.personalInformation = personalInformation

After using apply():

Customer().apply {
    accountType = "none"
    billingAddress = Address().apply {
        additionalInfo = "b"
        city = "Monument Valley"
        countryCode = "US"
        houseNumber = "13"
        state = "Utah"
        street = "Desertroad"
        zip = "84536"
    }
    locale = "en_US"
    merchantCustomerId = "1234"
    personalInformation = PersonalInformation().apply {
        dateOfBirth = "19490917"
        gender = "male"
        name = PersonalName().apply {
            firstName = "Wile"
            surname = "Coyote"
            surnamePrefix = "E."
            title = "Mr."
        }
    }
}

Conclusion

This post has just touched the tip of the iceberg on the expressivity power of Kotlin. With just a very small function like apply(), the readability of our code has been improved considerably. Working with deeply nested Java object is no longer a hassle in Kotlin.

Top comments (0)