kotlin-hints is a collection of Kotlin hints/idioms/patterns.
Writing idiomatic Kotlin code can be hard, especially when just learning the language.
More importantly, it's not always clear in which ways can existing Kotlin code be improved to become either shorter, faster, more readable or more maintainable.
There are good tools which help with that, including Intellij IDEA inspections and detekt, the static code analysis tool.
This project attempts to go beyond and lists the patterns which are not covered by the above tools.
Some of the patterns are not covered by these tools by design, however the list will also contain the items which are not yet implemented in these tools.
Most of the examples are taken from the Kotlin Language Reference.
Table of Contents
- kotlin-hints
Utility class could be replaced with function extensions.
// [Before]
class StringUtils {
fun swapCase(str: String): String { ... }
}// [After]
fun String.swapCase(): String { ... }
"Hello World".swapCase()Commonly used expressions made on a class property can be added to a class as an extension property instead.
// [Before]
println("Last index: ${list.size - 1}")
// [After]
val <T> List<T>.lastIndex: Int
get() = size - 1
println("Last index: ${list.lastIndex}")Could be converted to the expression body. Return type could then be inferred.
NB: Intellij IDEA has intentions for:
- Converting between a block and expression body.
- Specifying/removing an explicit return type.
// [Before]
fun sum(a: Int, b: Int): Int {
return a + b
}
// [After]
fun sum(a: Int, b: Int) = a + bSame applies for if and when, as these are expressions in Kotlin.
// [Before]
fun maxOf(a: Int, b: Int): Int {
return if (a > b) a else b
}
// [After]
fun maxOf(a: Int, b: Int) = if (a > b) a else b// [Before]
fun countryCode(country: String): String {
return when(country){
"China" -> "CHN"
"Japan" -> "JPN"
else -> throw UnsupportedOperationException("Unknown country: $country")
}
}
// [After]
fun countryCode(country: String) = when(country){
"China" -> "CHN"
"Japan" -> "JPN"
else -> throw UnsupportedOperationException("Unknown country: $country")
}Overloading methods with the reduced number of arguments can be avoided by providing default values for parameters.
// [Before]
fun printNumber() = printNumber(0)
fun printNumber(a: Int) = println("Number: $a")
// [After]
fun printNumber(a: Int = 0) = println("Number: $a")Functions can be nested. Inner function can access local variables of an outer function. Could be used when a helper function should be only visible to a single caller.
// [Before]
fun printReceipt(name: String, price: BigDecimal, bonusPoints: Int) {
printProduct(name, price)
printDiscount(bonusPoints)
}
private fun printProduct(name: String, price: BigDecimal) = println("Name: $name, Price: $price")
private fun printDiscount(bonusPoints: Int) = println("Collected $bonusPoints bonus points")
// [After]
fun printReceipt(name: String, price: BigDecimal, bonusPoints: Int) {
fun printProduct() = println("Name: $name, Price: $price")
fun printDiscount() = println("Collected $bonusPoints bonus points")
printProduct()
printDiscount()
}Function parameters can be named when calling a function.
fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {
...
}
// [Before]
reformat("hello,world", false, false, true, ',')
// [After]
reformat("hello,world", normalizeCase = false, upperCaseFirstLetter = false, divideByCamelHumps = true, wordSeparator = ',')Allows using functions in the form of a rem b instead of a.rem(b).
// [Before]
1.rem(2)
// [After]
infix fun Int.rem(num: Int) = this.rem(num)
1 rem 2Could use an implicit name it to simplify an expression.
NB: Intellij IDEA has an intention that performs the following transformation.
// [Before]
numbers.filter { n -> n > 0 }
// [After]
numbers.filter { it > 0 }Avoid writing a fully qualified name for the class in the scenario of an import name clash by disambiguating a class with a keyword as.
val utilDate: Date
val sqlDate: java.sql.Date
// [After]
import java.sql.Date as SqlDate
...
val utilDate: Date
val sqlDate: SqlDateCould destructure a data class when only it's internals are used within a scope.
data class User(val name: String = "", val age: Int = 0)
val alice = User("Alice", 22)
val ageLimit = 18
// [Before]
println("Name: $alice.name, Age: $alice.age")
if (alice.age >= ageLimit) println("$alice.name is over $ageLimit")
// [After]
val (name, age) = alice
println("Name: $name, Age: $age")
if (age >= ageLimit) println("$name is over $ageLimit")Could be replaced with a safe access expression.
NB: Intellij IDEA has intentions for converting between these two idioms.
// [Before]
fun middleNameLength(middleName: String?) = if (middleName != null) middleName.length else null
// [After]
fun middleNameLength(middleName: String?) = middleName?.lengthType checks can be used in when expressions.
// [Before]
if (obj is String) {
println("Text: $obj")
} else if (obj is Number) {
println("Number: $obj")
} else {
println("Unknown: $obj")
}
// [After]
when (obj) {
is String -> println("Text: $obj")
is Number -> println("Number: $obj")
else -> println("Unknown: $obj")
}Destructure a map to use key and value directly.
// [Before]
map.forEach { entry -> println("${entry.key} -> ${entry.value}") }
// [After]
map.forEach { (k, v) -> println("$k -> $v") }Use until operator to exclude the upper limit.
// Printing out numbers from 1 up to but excluding 10 (half-open range)
const val LIMIT = 10
// [Before]
for (i in 1..(LIMIT - 1)) println(i)
// [After]
for (i in 1 until LIMIT) println(i)with expression could be used.
// [Before]
fun printMenu(pizzas: List<String>) {
pizzas.forEach { println("Large $it") }
pizzas.forEach { println("Medium $it") }
pizzas.forEach { println("Small $it") }
}
// [After]
fun printMenu(pizzas: List<String>) = with(pizzas) {
forEach { println("Large $it") }
forEach { println("Medium $it") }
forEach { println("Small $it") }
}