Useful Kotlin Idioms You Should Know
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.
Let’s start with data classes, which are probably one of the most popular features of Kotlin.
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.
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:
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
With 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. Instead of adding a secondary constructor, we can just set a default value for the default color:
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:
In fact, we have seen the use of named arguments with the copy() function for data classes:
The copy() function arguments have all the default values. When invoking the function you can choose which parameters you want to provide:
For instance, instead of
you can write
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:
But it’s much cleaner with a “when” expression:
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.
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.
The function is quite useful for object initialization. For instance, here’s a nice example from Phillip Hauer’s blog:
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:
To implement the same in Kotlin, we can use the apply() function as follows:
You can learn about using Testcontainers with Kotlin in this Spring Time in Kotlin episode about integration testing.
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!
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.
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:
Or, if you would like to signal the incorrect situation, it is possible to use the Elvis operator (?:) as follows:
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:
This is so verbose! You can make this code much nicer by using the safe-call and Elvis operators:
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.
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:
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:
Since there’s just one statement in the function, you can apply the expression-body syntax as follows:
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.
Now you can invoke the new function on the Person class instance:
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.
This is not an exhaustive list of Kotlin idioms, but rather the basics to help you to get started with learning the idiomatic Kotlin style. Meanwhile, our team is working on collecting more idioms in the official documentation. Recently we published a page with the collection of Kotlin idioms for working with Strings. We invite you to share the Kotlin idioms you find useful either in the comments below or on Twitter by mentioning the @kotlin account.
What to read next:
Subscribe to Blog updates
Thanks, we've got you!
Kotlin for WebAssembly Goes Alpha
Kotlin/Wasm is in Alpha! Continue reading to explore the capabilities of this technology.
News Digest: Kotlin Multiplatform Special
Catch up on all the highlights in our news digest dedicated to the stories in the Kotlin Multiplatform ecosystem.
Tackle Advent of Code 2023 With Kotlin and Win Prizes!
Unwrap the joy of coding challenges as we gear up for Advent of Code, which JetBrains is proud to be sponsoring for a third consecutive year! Starting December 1, the JetBrains community will be diving into 25 days of coding challenges at adventofcode.com, and we warmly invite you to participate using Kotlin.