Why Kotlin? My experiences with this language in the past 1,5 years

-

For my current project at Bol.com, I worked on two business critical applications for the Platform Quality department. These applications apply business rules on the assortment of partner retailers to ensure the quality of the Bol.com platform. At peak hours, thousands of offers per second are being approved or rejected. The apps are being developed in Kotlin, and for me it was the first time I worked with this language in ‘real’ applications. In this article I’ll explain what made me love Kotlin and what are the main benefits of this language in my opinion.

 

Null-safety 

Kotlin is null-safe by design. Of course you can choose to create nullable values, but it is always a conscious decision to do this. When you create nullable values it will lead to compiler errors when you do not handle them correctly. Also, the compiler knows when a value cannot be null and you don’t need to care about it possibly being null.

 

Examples:

To create a value that can hold null, in Koltin you declare the variable with a ?, for example var a: String?. ‘a’ can be set to null if you want. To check a nullable property, there are some different options:

  • First you can explicitly check if ‘a’ is null:  if (a == null ) {…}
  • The second option is the safe call operator ?.. For example println(a?.length) will print a.length if a is not null and null otherwise. These safe call operators can also be chained.
  • Third, you can use let in combination with the safe call operator to perform an operation only on non-null values: a?.let { println(it) }
  • The Elvis Operator ?: can be used when you want to use ‘a’ if it is not null and otherwise use some non-null value: val l = a?.length ?: -1
  • The !! Operator can be used when you want to convert a nullable value into a non-null type and throw an exception when the value is null: val l = a!!.length will cause an NPE when a is null.

At my current project, the null-safety of Kotlin really helped us out when it turned out that a property of an object that was requested from another API turned out to be nullable, in contrast to what we would expect it to be. After we added the ? to indicate it was nullable, the compiler pointed out the functions we needed to adjust to handle this nullability.

 

Extension functions

In our microservice applications at Bol.com, we have a controller layer, service layer and data layer. We don’t want the service layer to have any ‘knowledge’ about the controller layer but we do want some mapping functions to convert the domain objects to api objects. For this we used Extension functions. Extension functions are used to extend a class with new functionality, without having to inherit from the class.

To use extension functions, you prefix the function’s name with the type being extended, the ‘receiver type’. Our object mapping functions look like:

fun Comment.toApi() = CommentDTO(id, author, text)
fun ProductPolicy.toApi() = ProductPolicyDTO(policy.toApi(), userName, comment?.toApi(), created)
return policyService.findById(id)?.toApi() ?: throw ResourceNotFoundException("Policy with id $id not found.")

Also note the safe operator in for example comment?.toApi(). The extension function is only being called if comment is not null. In the latest example, it is only being called if the response of policyService.findById(id) is not null. If it is not null, an exception is being thrown by using the Elvis Operator.

 

Type inference

Kotlin has the concept of Type Inference, which means that you don’t need to explicitly specify the type of a variable or return type of a function but it is inferred by the compiler. There are two types of type inference:

  • Local type inference for expressions locally in statement or expression scope. For example
    val policies = policyService.findAll()

    A pitfall is that this can hide the type of the variable which can lead to less readable code. The solution for this is to name your variables properly. A list of Policy objects should be named ‘policies’ and not ‘list’ for example. Also, of course your IDE can tell you the type when you want to know. When you really want it or when it is needed if the type cannot be inferred, there is also an option to specify the type:

    val policies: List<Policy> = policyService.findAll()
  • Function signature type inference, for inferring types of functions return values and/or parameters. For example:
    Instead of

    fun sum(a: Int, b: Int) : Int {return a + b}

    you can write

    fun sum(a: Int, b: Int) = a + b

    The advantage is that the code looks shorter and cleaner, but sometimes it can be unclear what the return type is. Also, there is a danger that your method ‘inherits’ a type change from a calling method. A solution for this can be use shorthand functions with an explicit return type:

    fun sum(a: Int, b: Int) : Int = a + b

 

Immutable

A reference in Kotlin is either a Variable (var) or a Value (val). The difference between these two is that a var can be reassigned after its definition and a val not. The same as for nullable values, you as a developer make the (hopefully conscious) decision about the mutability of your objects.

At Bol.com, sometimes we needed to change for example a list that was a variable of an object. To do this with a val, you can use the ‘copy’ method:

return policy.copy(
    rules = rulesSorted,
    sets = setsSorted
)

 

Data classes 

Instead of declaring a class with properties and getters and setters for classes that hold data, in Kotlin you use data classes. When you mark a class with ‘data’, you can access it’s properties by just the name:

data class Policy(val id: String, val name: String)
val policy = Policy(“1”, “policy1”)

when you want to print the name, just write

println(policy.name)

When declaring a class as data class you get some other things for free as well:

  • Equals()/hashCode()
  • toString()
  • copy() functions for immutable types

 

No static keyword

Kotlin has no ‘static’ keyword. Why this is, I can’t really tell. You can use Kotlin’s Companion Objects when you need to declare an object inside a class:

class MyClass {
    companion object {
        const val CHUNK_SIZE = 50 
        fun create(): MyClass = MyClass()
    }
}

Members of the companion object can be called by using the class name as qualifier: val instance = Myclass.create(). Also, the const CHUNK_SIZE is created in this companion object, which can be used the same way as a static final Integer in Java.     

 

What makes life better

After working with Kotlin for 1,5 years, I see a lot of advantages of this language in just the way the code it clean and readable and in some nice syntactic features. Some examples:

 

Utility methods on collections

My favorite features in Kotlin are all the utility methods on Collections that Kotlin provides. I’ll not dive into every one of them, but will name some of the features that I used a lot in my current project.

  • Map: the mapping transformation creates a collection from the results of a function on the elements of another collection.
val data = dataRaw.map { it.trim().toLowerCase() }
  • ForEach: the forEach function iterates a collection and executes the given code for each element:
shops.forEach { 
    shopRepository.create(it)
}
  • Filter, filterNot: In Kotlin, filtering conditions are defined by predicates:
val allRules = policyRegistry.getRules()
allRules.filter { ruleNames.contains(it.name) }
    .map { it.errorCode.name }

          When you want to filter a collection by negative conditions, you can use filterNot:

val invalidProducts = uploadedProducts.filterNot { isValid(it.globalId) } 
  • Ordering: ordering a list in Kotlin is very easy. An example with two properties of an object to use for ordering:
val rulesSorted = policy.rules.sortedWith(
    compareBy<PolicyRuleDTO> { it.state }.thenBy { it.name }
) 
  • isEmpty(): a very basic operation, but what makes it nice is that there is also a isNotEmpty() which makes the code a lot more readable in my opinion then !isEmpty()   

 

String templating

Kotlin has built in String templating:

val name = "Sofie"
val children = listOf("Maxim", "Otis")
println("$name has ${children.size} children")

 

Multiline String constants

We use this for example in tests, when we want to post JSON to an endpoint:

val json = """
    {
        "name": "Sofie",
        "children": ["Maxim", "Otis"]
    }
"""

 

If/when expressions

In Kotlin if and when are expressions, which means they can be used inline to return a value. For example with if:

val max = if (a > b) a else b 

When is used to replace the switch statement:

fun validate(value: String, listData: ListData) : String {
    return when(listData) {
        ListData.BRICK -> validateBrick(value)
        ListData.CHUNK -> validateChunk(value)
        else -> value
    }
} 

When can also be used without argument, as a replacement for an if-else if chain:

private fun mapOnlyValid(product: Product?): Product? {
    fun notANumber(input: String) = !input.matches(NUMBER_REGEX)
    return when {
        product == null -> null
        notANumber(product.globalId) -> null
        notANumber(product.ean) -> null
        notANumber(product.brick) -> null
        notANumber(product.chunk) -> null
        else -> product
    }
} 

 

Conclusion

After working with Kotlin now for 1,5 years, I can say that I really like how it makes my code more compact but still readable. Every time I need to do a complex operation on a collection for example, and think that it must be possible in Kotlin, it turns out to be possible indeed. What I also like is the extensive documentation on kotlinlang.org with very helpful examples. With the help of this documentation and the features Intellij provides, it is not hard to learn when you are used to write Java. Just give it a try!