Skip to content

tigrik-dev/kotlin-hints

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kotlin-hints

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

Extensions

Utility class

Utility class could be replaced with function extensions.

// [Before]
class StringUtils {
    fun swapCase(str: String): String { ... }
}
// [After]
fun String.swapCase(): String { ... }

"Hello World".swapCase()

Extension property

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}")

Functions

Single expression in a block body

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 + b

Same 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")
}

Method overload

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")

Local function

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()
}

Named arguments

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 = ',')

Infix function

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 2

Lambdas

Single parameter

Could 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 }

Imports

Name clash

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: SqlDate

Data classes

Destructure

Could 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")

If expressions

If not null

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?.length

When expressions

Type check

Type 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")
}

Maps

Traverse keys and values

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") }

Ranges

Exclusive upper limit

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 expressions

Multiple method calls on an object

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") }
}

About

A collection of Kotlin hints/idioms/patterns that are not covered by Intellij IDEA inspections and static code analysis tools

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages