Kotlin Idioms and Best Practices
Idiomatic Kotlin is code that uses the language's features in the way they were intended. Idiomatic code is shorter, safer, and easier to understand. This topic covers the most important patterns every Kotlin developer should follow.
1. Prefer val Over var
// Avoid — mutable by default
var name = "Alice"
var count = 0
// Prefer — immutable by default, switch to var only when needed
val name = "Alice"
var count = 0 // only var because it actually changes2. Use Data Classes for Plain Data
// Avoid — manual boilerplate
class User(val name: String, val age: Int) {
override fun equals(other: Any?) = ...
override fun hashCode() = ...
override fun toString() = ...
}
// Prefer — data class generates everything
data class User(val name: String, val age: Int)3. Use when Instead of Long if-else Chains
// Avoid
fun getLabel(code: Int): String {
if (code == 200) return "OK"
else if (code == 404) return "Not Found"
else if (code == 500) return "Server Error"
else return "Unknown"
}
// Prefer
fun getLabel(code: Int) = when (code) {
200 -> "OK"
404 -> "Not Found"
500 -> "Server Error"
else -> "Unknown"
}4. Use String Templates
val name = "Alice"
val score = 92
// Avoid
val msg = "Player " + name + " scored " + score + " points"
// Prefer
val msg = "Player $name scored $score points"
val detail = "Score: ${score * 1.1}" // expression with braces5. Use apply for Object Initialization
// Avoid — repeated variable reference
val request = HttpRequest()
request.url = "https://api.example.com"
request.method = "POST"
request.timeout = 5000
// Prefer — apply groups all setup
val request = HttpRequest().apply {
url = "https://api.example.com"
method = "POST"
timeout = 5000
}6. Use let for Null-Safe Calls
val email: String? = getUserEmail()
// Avoid
if (email != null) {
sendEmail(email)
}
// Prefer
email?.let { sendEmail(it) }
// With transformation and fallback
val domain = email?.let { it.substringAfter("@") } ?: "unknown.com"7. Use Elvis for Default Values
// Avoid
val city = if (user.city != null) user.city else "Unknown"
// Prefer
val city = user.city ?: "Unknown"
val count = list?.size ?: 08. Destructure Where Possible
// Avoid
for (entry in map) {
println(entry.key + ": " + entry.value)
}
// Prefer
for ((key, value) in map) {
println("$key: $value")
}9. Use Extension Functions for Clean APIs
// Avoid — utility function that takes String as argument
fun isValidEmail(email: String) = email.contains("@")
// Prefer — reads like a natural method on String
fun String.isValidEmail() = contains("@") && contains(".")
"alice@mail.com".isValidEmail() // true10. Use Sealed Classes for State
// Avoid — error-prone flags
var isLoading = false
var data: List- ? = null
var errorMessage: String? = null
// Prefer — exhaustive and type-safe
sealed class UiState {
object Loading : UiState()
data class Success(val data: List
- ) : UiState()
data class Error(val message: String) : UiState()
}
11. Avoid !! — Use Safe Alternatives
// Avoid — crash waiting to happen
val length = name!!.length
// Prefer — handle null explicitly
val length = name?.length ?: 0
val upper = name?.uppercase() ?: return12. Use Collection Functions Over Loops
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Avoid
val evens = mutableListOf()
for (n in numbers) {
if (n % 2 == 0) evens.add(n * n)
}
// Prefer
val evens = numbers.filter { it % 2 == 0 }.map { it * it } 13. Use Named Arguments for Clarity
// Avoid — hard to read
createUser("Alice", "alice@mail.com", true, 3)
// Prefer — self-documenting
createUser(
name = "Alice",
email = "alice@mail.com",
isAdmin = true,
maxPosts = 3
)14. Keep Functions Small and Focused
// Avoid — does too many things
fun processOrder(order: Order) {
validateOrder(order)
calculateTotal(order)
applyDiscount(order)
sendConfirmationEmail(order)
updateInventory(order)
}
// Prefer — each function does one thing
fun processOrder(order: Order) {
val validated = validate(order)
val priced = calculatePricing(validated)
val fulfilled = fulfill(priced)
notify(fulfilled)
}