Kotlin with Style
or: A quick history of code styling and formatting in kotlin
Most of the code I work with on a daily basis is written in kotlin. And Kotlin always had what feels to me like very intuitive syntax and loads of features that make code more enjoyable to read - especially when coming from the Java world.
When I started working with Kotlin at the end of 2015 – before version 1.0 was released in February 2016 1 – there was one thing missing. And to me, that was a common, (more or less) universally accepted set of coding conventions that can be automatically checked or applied. This lack of established coding guidelines of course mostly had to do with Kotlin being a young, but quickly maturing hip programming language.
The official set of coding conventions2, and the accompanying active contributions and discussions by the community in the accompanying GitHub repository3 provided some initial guidance, but things really changed quickly when Google announced that Android will support Kotlin in 20174 and bundle the Kotlin plugin in all upcoming Android Studio releases automatically. This announcement, and the subsequent further encouragement from Google’s side towards Kotlin-based Android development helped greatly increase the number of Kotlin developers.
Not surprisingly, nowadays a lot of tooling for Kotlin is therefore focused on mobile development first and foremost, although since the introduction of Kotlin support in the Spring project5, the advent of other Kotlin based backend technologies (e.g. ktor) and the growing support for Multiplatform development in Kotlin, the language nowadays isn’t necessarily only relevant for use in mobile projects. In 2020, more than half of all Kotlin users already does some form of development on the server side using Kotlin as well, although it is still used more for mobile development, as can be seen in the following figure.
Nowadays, the most used code-style documentations for Kotlin are probably still the aforementioned – and now grown in size – official Kotlin coding conventions as well as the Android Kotlin style guide. These two sets of coding conventions are mostly analogous, and if you’re using IntelliJ to develop your Kotlin code, you per default already checking your code against the official Kotlin coding conventions. As the following figure shows, the vast majority of Kotlin users write code on an IntelliJ based IDE, hence we can probably assume the default IntelliJ code style is the most widely used code style for Kotlin development, since more than half of Kotlin code is right now written for personal projects and - let’s be honest here - nobody cares about setting up special custom code styles for personal projects.
For me personally, analogue to the maturing of the Kotlin language itself, the most relevant change is the growing support of Kotlin in existing toolchains and the integration of new Kotlin tools that fill previously open gaps. For instance, the combination of Java and Kotlin-based annotation processors (apt
vs kapt
) in Android gradle builds was a hassle, but nowadays most tools have been integrated well enough into the existing ecosystem. Two more pieces of tooling I’d like to highlight are Pinterest’s ktlint and detekt, which help analyse code smells, formatting mistakes and code complexity problems automatically. As opposed to the IDE-based code formatting, detekt (which uses ktlint itself) can also be easily integrated into build toolchains, pre-commit hooks and CLI scripts.
Some things code style (and automation) can’t yet solve
While it seems code style (i.e. the formatting part) has been less and less of a problem when it comes to maintaining codebases, there still are some hiccups automated tools can’t yet resolve automatically (even though tools like detekt provide a lot of help in at least showing them). One of the things that very often shows up in code reviews that I do is what I’d call unnecessary nesting
. From my experience, it is especially common to see this happen in validating parameters that are sent by users into our system, for instance in a REST service - as the following example illustrates.
data class LineItem(val quantity: Int, val product: String)
@PostMapping("/buy")
fun buyItems(@RequestBody lineItems: List<LineItem>): Receipt {
if(lineItems.isNotEmpty()) {
if(lineItems.none { it.product.isEmpty() }) {
return paymentService.buyItems(lineItems)
} else {
throw LineItemIsEmptyException()
}
} else {
throw EmptyLineItemsException()
}
}
This example, for instance is perfectly fine as far as automatically checked coding conventions are concerned, although at least in my opinion, there’s still room for improvement. Also, while this is just a small example, checking for parameter validity can sometime be a longer process that’s not just resolved in one or two if
statements - such that the nesting can become deeper and deeper.
At first glance it also seems as if IntelliJ can provide us with some relief by allowing us to do automatic code improvements. In this example, for instance, IntelliJ allows us to do one of two things. It can automatically delete braces from the else branches - which might make the code look a bit cleaner but doesn’t really solve the underlying problem of the nested if
statements.
Similarly, the other suggested option - lifting the return out of the nested statements - doesn’t really solve the problem, but just makes it look a bit less obvious.
It seems the problem is that automated tooling in this case does not realize that nesting the statements is completely unnecessary, since every single block in the if
or else
branches terminates the function (either by returning or throwing an error) anyways. It follows, that we can just rewrite the function as follows, in order to make this look a lot cleaner, readable without the need to change any logic (except for flipping the conditions in the ifs). Without talking code, we could argue that in general it is a lot cleaner to first focus on filtering out all possible exceptional values, terminating with errors (or specific responses, whatever you prefer) all at the start, and subsequently implement the real functionality. A cleaner, rewritten version of the previous example is shown below:
data class LineItem(val quantity: Int, val product: String)
@PostMapping("/buy")
fun buyItems(@RequestBody lineItems: List<LineItem>): Receipt {
if (lineItems.empty()) throw EmptyLineItemsException()
if (lineItems.any { it.product.isEmpty() })
throw LineItemIsEmptyException()
return paymentService.buyItems(lineItems)
}
Of course, this is not a problem limited to one single language, but this example just goes to show there’s a limit on what automated tooling can solve as of now. And it looks like for a long time we still will do code reviews, although they might be more and more aided by technological tooling.