Philipp Hauer's Blog

Engineering Management, Java Ecosystem, Kotlin, Sociology of Software Development

Idiomatic Kotlin. Best Practices.

Posted on Mar 28, 2017. Updated on Jun 12, 2022

In order to take full advantage of Kotlin, we have to revisit some best practices we got used to in Java. Many of them can be replaced with better alternatives that are provided by Kotlin. Let’s see how we can write idiomatic Kotlin code and do things the Kotlin way.

Idiomatic Kotlin. Best Practices.

A word of warning: The following list is not exhaustive and does only express my humble opinion. Moreover, some Kotlin features should be used with sound judgment. If overused, they can make our code even harder to read. For instance, when you create a “train wreck” by trying to squeeze everything into a single unreadable expression.

Kotlin’s Built-in Support for Common Java Idioms and Patterns

In Java, we have to write quite some boilerplate code to implemented certain idioms and patterns. Fortunately, many patterns are built-in right into Kotlin’s language or its standard library.

Java Idiom or Pattern Idiomatic Solution in Kotlin
Optional Nullable Types
Getter, Setter, Backing Field Properties
Static Utility Class Top-Level (extension) functions
Immutability data class with immutable properties, copy()
Value Objects inline class with immutable properties
Fluent Setter (Wither) Named and default arguments, apply()
Method Chaining Default arguments
Singleton object
Delegation Delegated properties by
Lazy Initialization (thread-safe) Delegated properties by: lazy()
Observer Delegated properties by: Delegates.observable()

Functional Programming

Among other advantages, functional programming allows us to reduce side-effects, which in turn makes our code…

  • less error-prone,
  • easier to understand,
  • easier to test and
  • thread-safe.

In contrast to Java 8, Kotlin has way better support for functional programming:

  • Immutability: val for variables and properties, immutable data classes, copy()
  • Expressions: Single expression functions. if, when and try-catch are expressions. We can combine these control structures with other expressions concisely.
  • Function Types
  • Concise Lambda Expressions
  • Kotlin’s Collection API

These features allow writing functional code in a safe, concise and expressive way. Consequently, we can create pure functions (functions without side-effects) more easily.

Use Expressions

// Don't
fun getDefaultLocale(deliveryArea: String): Locale {
    val deliverAreaLower = deliveryArea.toLowerCase()
    if (deliverAreaLower == "germany" || deliverAreaLower == "austria") {
        return Locale.GERMAN
    }
    if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") {
        return Locale.ENGLISH
    }
    if (deliverAreaLower == "france") {
        return Locale.FRENCH
    }
    return Locale.ENGLISH
}
// Do
fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) {
    "germany", "austria" -> Locale.GERMAN
    "usa", "great britain" -> Locale.ENGLISH
    "france" -> Locale.FRENCH
    else -> Locale.ENGLISH
}

Rule of thumb: Every time you write an if consider if it can be replaced with a more concise when expression.

try-catch is also a useful expression:

val json = """{"message":"HELLO"}"""
val message = try {
    JSONObject(json).getString("message")
} catch (ex: JSONException) {
    json
}

Top-Level (Extension) Functions for Utility Functions

In Java, we often create static util methods in util classes. A direct translation of this pattern to Kotlin would look like this:

//Don't
object StringUtil {
    fun countAmountOfX(string: String): Int{
        return string.length - string.replace("x", "").length
    }
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")

Kotlin allows removing the unnecessary wrapping util class and use top-level functions instead. Often, we can additionally leverage extension functions, which increases readability. This way, our code feels more like “telling a story”.

//Do
fun String.countAmountOfX(): Int {
    return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX()

Named Arguments instead of Fluent Setter

Back in Java, fluent setters (also called “Wither”) where used to simulate named and default arguments and to make huge parameter lists more readable and less error-prone:

//Don't
val config = SearchConfig()
       .setRoot("~/folder")
       .setTerm("game of thrones")
       .setRecursive(true)
       .setFollowSymlinks(true)

In Kotlin, named and default arguments fulfil the same propose but are built directly into the language:

//Do
val config2 = SearchConfig2(
       root = "~/folder",
       term = "game of thrones",
       recursive = true,
       followSymlinks = true
)

apply() for Grouping Object Initialization

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

The extension function apply() helps to group and centralize initialization code for an object. Besides, we don’t have to repeat the variable name over and over again.

//Do
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
}

apply() is often useful when dealing with Java libraries in Kotlin.

Don’t Overload for Default Arguments

Don’t overload methods and constructors to realize default arguments (so called “method chaining” or “constructor chaining”).

//Don't
fun find(name: String){
    find(name, true)
}
fun find(name: String, recursive: Boolean){
}

That is a crutch. For this propose, Kotlin has named arguments:

//Do
fun find(name: String, recursive: Boolean = true){
}

In fact, default arguments remove nearly all use cases for method and constructor overloading in general, because overloading is mainly used to create default arguments.

Concisely Deal with Nullability

Avoid if-null Checks

The Java way of dealing with nullability is cumbersome and easy to forget.

//Don't
if (order == null || order.customer == null || order.customer.address == null){
    throw IllegalArgumentException("Invalid Order")
}
val city = order.customer.address.city

Every time you write an if-null check, hold on. Kotlin provides much better ways to handle nulls. Often, you can use a null-safe call ?. or the elvis operator ?: instead.

//Do
val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order")

Avoid if-type Checks

The same is true for if-type-check.

//Don't
if (service !is CustomerService) {
    throw IllegalArgumentException("No CustomerService")
}
service.getCustomer()

Using as? and ?: we can check the type, (smart-)cast it and throw an exception if the type is not the expected one. All in one expression!

//Do
service as? CustomerService ?: throw IllegalArgumentException("No CustomerService")
service.getCustomer()

Avoid not-null Assertions !!

//Don't
order!!.customer!!.address!!.city

“You may notice that the double exclamation mark looks a bit rude: it’s almost like you’re yelling at the compiler. This is intentional. The designers of Kotlin are trying to nudge you toward a better solution that doesn’t involve making assertions that can’t be verified by the compiler." Kotlin in Action by Dmitry Jemerov and Svetlana Isakova

Consider let()

Sometimes, using let() can be a concise alternative for if. But you have to use it with sound judgment in order to avoid unreadable “train wrecks”. Nevertheless, I really want you to consider using let().

val order: Order? = findOrder()
if (order != null){
    dun(order.customer)
}

With let(), there is no need for an extra variable. So we get along with one expression.

findOrder()?.let { dun(it.customer) }
//or
findOrder()?.customer?.let(::dun)

Leverage Value Objects

With data classes, writing immutable value objects is so easy. Even for value objects containing only a single property. So there is no excuse for not using value objects anymore!

// Don't
fun send(target: String){}

// Do
fun send(target: EmailAddress){}
// expressive, readable, type-safe

data class EmailAddress(val value: String)
// Even better (Kotlin 1.3):
inline class EmailAddress(val value: String)

Since Kotlin 1.3, we should use inline classes for value objects. This way, we avoid the overhead of additional object creation because the compiler removes the wrapping inline class and uses the wrapped property directly. So it’s a free abstraction.

Concise Mapping with Single Expression Functions

// Don't
fun mapToDTO(entity: SnippetEntity): SnippetDTO {
    val dto = SnippetDTO(
            code = entity.code,
            date = entity.date,
            author = "${entity.author.firstName} ${entity.author.lastName}"
    )
    return dto
}

With single expression functions and named arguments we can write easy, concise and readable mappings between objects.

// Do
fun mapToDTO(entity: SnippetEntity) = SnippetDTO(
        code = entity.code,
        date = entity.date,
        author = "${entity.author.firstName} ${entity.author.lastName}"
)
val dto = mapToDTO(entity)

If you prefer extension functions, you can use them here to make both the function definition and the usage even shorter and more readable. At the same time, we don’t pollute our value object with the mapping logic.

// Do
fun SnippetEntity.toDTO() = SnippetDTO(
        code = code,
        date = date,
        author = "${author.firstName} ${author.lastName}"
)
val dto = entity.toDTO()

Refer to Constructor Parameters in Property Initializers

Think twice before you define a constructor body (init block) only to initialize properties.

// Don't
class UsersClient(baseUrl: String, appName: String) {
    private val usersUrl: String
    private val httpClient: HttpClient
    init {
        usersUrl = "$baseUrl/users"
        val builder = HttpClientBuilder.create()
        builder.setUserAgent(appName)
        builder.setConnectionTimeToLive(10, TimeUnit.SECONDS)
        httpClient = builder.build()
    }
    fun getUsers(){
        //call service using httpClient and usersUrl
    }
}

Note that we can refer to the primary constructor parameters in property initializers (and not only in the init block). apply() can help to group initialization code and get along with a single expression.

// Do
class UsersClient(baseUrl: String, appName: String) {
    private val usersUrl = "$baseUrl/users"
    private val httpClient = HttpClientBuilder.create().apply {
        setUserAgent(appName)
        setConnectionTimeToLive(10, TimeUnit.SECONDS)
    }.build()
    fun getUsers(){
        //call service using httpClient and usersUrl
    }
}

object for Stateless Interface Implementations

Kotlin’s object comes in handy when we need to implement a framework interface that doesn’t have any state. For instance, Vaadin 8’s Converter interface.

//Do
object StringToInstantConverter : Converter<String, Instant> {
    private val DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss Z")
            .withLocale(Locale.UK)
            .withZone(ZoneOffset.UTC)

    override fun convertToModel(value: String?, context: ValueContext?) = try {
        Result.ok(Instant.from(DATE_FORMATTER.parse(value)))
    } catch (ex: DateTimeParseException) {
        Result.error<Instant>(ex.message)
    }

    override fun convertToPresentation(value: Instant?, context: ValueContext?) =
            DATE_FORMATTER.format(value)
}

For further information about the synergies between Kotlin, Spring Boot and Vaadin, check out this blog post.

Destructuring

On the one hand, destructuring is useful for returning multiple values from a function. We can either define an own data class (which is the preferred way) or use Pair (which is less expressive, because Pair doesn’t contain semantics).

//Do
data class ServiceConfig(val host: String, val port: Int)
fun createServiceConfig(): ServiceConfig {
    return ServiceConfig("api.domain.io", 9389)
}
//destructuring in action:
val (host, port) = createServiceConfig()

On the other hand, destructuring can be used to concisely iterate over a map:

//Do
val map = mapOf("api.domain.io" to 9389, "localhost" to 8080)
for ((host, port) in map){
    //...
}

Ad-Hoc Creation of Structs

listOf, mapOf and the infix function to can be used to create structs (like JSON) quite concisely. Well, it’s still not as compact as in Python or JavaScript, but way better than in Java.

//Do
val customer = mapOf(
        "name" to "Clair Grube",
        "age" to 30,
        "languages" to listOf("german", "english"),
        "address" to mapOf(
                "city" to "Leipzig",
                "street" to "Karl-Liebknecht-Straße 1",
                "zipCode" to "04107"
        )
)

But usually, we should use data classes and object mapping to create JSON. But sometimes (e.g. in tests) this is very useful.

Sealed Classes Instead of Exceptions

Especially for remote calls (like HTTP requests) the usage of a dedicated result class hierarchy can improve the safety, readability and traceability of the code.

// Definition
sealed class UserProfileResult {
    data class Success(val userProfile: UserProfileDTO) : UserProfileResult()
    data class Error(val message: String, val cause: Exception? = null) : UserProfileResult()
}

// Usage
val avatarUrl = when (val result = client.requestUserProfile(userId)) {
    is UserProfileResult.Success -> result.userProfile.avatarUrl
    is UserProfileResult.Error -> "http://domain.com/defaultAvatar.png"
}

Contrary to exceptions (which are always unchecked in Kotlin), the compiler guides you to handle the error cases. If you use when as an expression the compiler even forces you to handle the error case. If like to read more about sealed classes as an alternative to exceptions, check out the post ‘Sealed Classes Instead of Exceptions’.

Source Code

You can find the source code in my GitHub project idiomatic kotlin.

Further Reading