Blog

  • Home / Android / Metacoder: Useful Kotlin Expression You Should Know

Metacoder: Useful Kotlin Expression You Should Know

  • August 12, 2021

Kotlin was designed to be very similar to Java to make migration as smooth as possible. However, Kotlin was also designed to improve the developers’ experience by providing a more expressive syntax and a more sophisticated type system. To take full advantage of the language and write more concise code, learning Kotlin idioms is a must. Without them, it is easy to fall back into old Java patterns.

So, where do you begin? In this blog post, we would like to highlight some good places to start on the path to learning idiomatic Kotlin.

Data classes

If you need a class to hold some values, data classes are exactly what you need. The main purpose of data classes is to hold data, and some utility functions are automatically generated for them.

data class Figure(
   val width: Int,
   val height: Int,
   val length: Int,
   val color: Color,
)


In addition to equals()hashCode(), and toString() functions, data classes include a very convenient copy() function and a way to destructure the object into number of variables:

val figure = Figure(1, 2, 3, Color.YELLOW)
val (w, h, l, _) = figure.copy(color = Color.RED)


See how we only redefine the color in the copy function for the figure? This is a nice introduction to the next two Kotlin features – named and default arguments.

Named and default arguments

we can remove the need for overloading constructors or functions. 
For example, let’s say we’d like to create instances of the Figure class from the above example with the default color set to GREEN.

data class Figure(
   val width: Int,
   val height: Int,
   val length: Int,
   val color: Color = Color.GREEN,
)

val figure = Figure(1, 2, 3)


When we read this code, it is difficult to immediately figure out what the Figure’s constructor arguments are. What do 1, 2, and 3 mean? The IDE can help us out by rendering the parameter names as hints:


To improve the readability of such code, you can also use named arguments:


val figure = Figure(width = 1, height = 2, length = 3)


In fact, we have seen the use of named arguments with the copy() function for data classes:

val greenFigure = Figure(1, 2, 3) // default color is GREEN
val redFigure = greenFigure.copy(color = Color.RED)


The copy() function arguments have all the default values. When invoking the function you can choose which parameters you want to provide:

... = greenFigure.copy(width = 10)
... = greenFigure.copy(color = Color.RED)
... = greenFigure.copy(width = 15, color = Color.YELLOW)


Expressions

An important thing to keep in mind when starting out with Kotlin after having programmed in Java is that ifwhen, and try are expressions in Kotlin. All of these expressions return a value. 

For instance, instead of

val weather: String = getWeatherConditions()
var drivingStyle = ""

if(weather == "Sunny") {
   drivingStyle = "Speedy"
} else {
   drivingStyle = "Safely"
}


you can write

val weather: String = getWeatherConditions()
var drivingStyle = if(weather == "Sunny") {
   "Speedy"
} else {
   "Safely"
}


When the conditions for the “if” statement are too complex, it is worth using the “when” expression. For instance, this code looks a bit noisy with “if” expressions: 

val weather: String = getWeatherConditions()
var drivingStyle =
   if (weather == "Sunny") {
       "Speedy"
   } else if (weather == "Foggy" || weather == "Rainy") {
       "Safely"
   } else if (weather == "Blizzard") {
       "Don't drive!"
   } else {
       "Undefined"
   }


But it’s much cleaner with a “when” expression:

var drivingStyle = when (getWeatherConditions()) {
   "Sunny" -> "Speedy"
   "Foggy", "Rainy" -> "Safely"
   "Blizzard" -> "Don't drive!"
   else -> "Undefined"
}


In combination with sealed classes (or enum classes), the “when” expression becomes a powerful tool to make your programs safer.

sealed class Weather
object Rainy : Weather()
object Sunny : Weather()
object Foggy : Weather()
object Blizzard : Weather()

var drivingStyle = when (getWeatherConditions()) {
   Sunny -> "Speedy"
   Foggy, Rainy  -> "Safely"
}


This code does not compile! We have to either add an “else” branch in the “when” statement or cover all the remaining options for the condition.


The issue is that using the else branch would diminish the benefits of using sealed classes in “when” expressions. If the else branch is present, adding the new subclass won’t result in a compilation error and you can miss the places where the specific case is required for the new subclass. In Detekt, for instance, you can configure whether or not the else branch can be treated as a valid case for enums and sealed classes.

apply()

In Kotlin, apply() is one of the five scope functions provided by the standard library. It is an extension function and it sets its scope to the object on which apply() is invoked. This allows executing any statements within the scope of the receiver object. In the end, the function returns the same object, with some modified changes.

val dataSource = BasicDataSource().apply {
   driverClassName = "com.mysql.jdbc.Driver"
   url = "jdbc:mysql://domain:3309/db"
   username = "username"
   password = "password"
   maxTotal = 40
   maxIdle = 40
   minIdle = 4
}


Instead of creating an object variable and referring to it for initializing every single property, we can assign the values within the block to the apply() function. 

The apply() function also comes in useful when working with Java libraries that use recursive generics. For instance, Testcontainers use recursive generics to implement self-typing to provide a fluent API. Here’s an example in Java:

PostgreSQLContainer<?> container = new PostgreSQLContainer<>("postgres:13")
       .withInitScript("schema.sql")
       .withDatabaseName("database")
       .withUsername("user")
       .withPassword("password");


To implement the same in Kotlin, we can use the apply() function as follows:

val container = PostgreSQLContainer<Nothing>(DockerImageName.parse("postgres:13")).apply {
    withDatabaseName("db")
    withUsername("user")
    withPassword("password")
    withInitScript("sql/schema.sql")
}


You can learn about using Testcontainers with Kotlin in this Spring Time in Kotlin episode about integration testing.

Null-safety

When talking about Kotlin’s features and idiomatic code, you can’t get around null-safety. An object reference might be null, and the compiler will let us know if we are trying to dereference the null value. That’s really convenient!

val figure: Figure? = createFigure() // can return null
val otherFigure = figure.copy(color = Color.YELLOW)


The figure.copy() is a potential source of NullPointerException, as the createFigure() function might have returned null. We could validate if the reference is null and then safely invoke the copy() function. 

val figure: Figure? = createFigure()
if(figure != null) {
   val otherFigure = figure.copy(color = Color.YELLOW)
}

// or

val figure: Figure? = createFigure() // can return null

if(figure == null) {
   throw IllegalStateException("figure is null")
}

val otherFigure = figure.copy(color = Color.YELLOW)


You can imagine that in more complex use cases, this code will become quite verbose and cluttered with null-checks. To remedy this, there are useful operators to deal with nullability in Kotlin.

First, you can use the safe-call operator:

val figure: Figure? = createFigure()
val otherFigure = figure?.copy(color = Color.YELLOW)


Or, if you would like to signal the incorrect situation, it is possible to use the Elvis operator (?:) as follows:

val figure: Figure = createFigure() ?: throw IllegalStateException("figure is null")
val otherFigure = figure.copy(color = Color.YELLOW)


If the result of the createFigure() function is null, then the Elvis operator will lead to throwing the IllegalArgumentException. This means that there’s no situation when the figure object could be null, so you can get rid of the nullable type and the compiler won’t complain about calling any function on this object. The compiler is now absolutely sure that there won’t be a NullPointerException.

When working with object graphs where any object could be null, you inevitably have to null-check the values.

For example, Bob is an employee who may be assigned to a department (or not). That department may in turn have another employee as a department head. To obtain the name of Bob’s department head (if there is one), you write the following:

val bob = findPerson()

if( bob == null || 
    bob.department == null || 
    bob.department.name == null){
   throw IllegalStateException("invalid data")
}

val name = bob.department.head.name


This is so verbose! You can make this code much nicer by using the safe-call and Elvis operators:

val bob = findPerson()
val name = bob?.department?.head?.name ?: throw IllegalStateException("invalid data")


By using the nullable types in Kotlin, you help the compiler to validate your programs, and so make the code safer. The additional operators, like safe-call and Elvis, let you work with the nullable types in a concise manner. You can find more information about null-safety in Kotlin on the documentation page.

Extension functions

In Java, static functions in Util-classes is a common idiom. ClientUtil, StringUtil, and so on – you have definitely encountered these in your Java projects. 

Consider the following example:

class Person(val name: String, val age: Int)

val person = Person("Anton", 16)
println(person) // org.kotlin.Person@6bf256fa


Because there’s no toString() function in the Person class, this code prints just an object reference value. You could either implement your own toString() function, or define Person as a data class to let the compiler generate this function for you. But what if you can’t modify the source of the Person class (e.g. the class is provided by an external library you have added to your project)?

If you use Java habits, you would probably create a PersonUtil class with a static function that takes the Person class as an argument and returns a String. In Kotlin, there’s no need to create *Util classes, as there are top-level functions available, so it would look something like this:

fun prettyPrint(person: Person): String {
   return "Person{name=" + person.name + ", age=" + person.age + "}"
}


You can use string templates instead of string concatenation:

fun prettyPrint(person: Person): String {
   return "Person{name=${person.name}, age=${person.age}}"
}


Since there’s just one statement in the function, you can apply the expression-body syntax as follows:

fun prettyPrint(person: Person): String =
   "Person{name=${person.name}, age=${person.age}}"


It’s getting better, but still looks quite like Java. You can improve this code by implementing prettyPrint() as an extension function to the Person class. You don’t need to modify the Person class source code for that, as the extension can be declared in a different location than the original class.

fun Person.prettyPrint(): String = "Person{name=$name, age=$age}"


Now you can invoke the new function on the Person class instance:

val person = Person("Anton", 16)
println(person.prettyPrint()) // Person{name=Anton, age=16}


By using the extension functions it is possible to extend existing APIs. For instance, when using Java libraries you can extend the existing classes with new functions for convenience. In fact, a number of functions in the Kotlin standard library are implemented via extensions. For instance, scope functions are a prominent example of extension functions in the standard library.

Happy Learning 🙂

-Metacoder