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.
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
andtry-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
- Idiomatic Unit Testing with Kotlin
- I highly recommend the book Kotlin in Action by Dmitry Jemerov and Svetlana Isakova