Idioms
See also: https://kotlinlang.org/docs/idioms.html
Utilize Kotlin's built-in features - write idiomatic code. This will make your code much simpler (especially simpler to read), and be less lines overall (thus faster to write).
Some examples:
Use Kotlin Null Safety features
// Good ✅
val length = str?.length ?: 0
val userCity = user?.address?.city?.uppercase() ?: "UNKNOWN"
data class Report(
val user: User?,
val statistics: Statistics?
) {
fun getFormattedStats() = statistics?.let { stats ->
"${stats.visits} visits, ${stats.conversion}% conversion"
} ?: "No statistics available"
}
// Avoid ❌ - Java-style null checking hell
val length = if (str != null) str.length else 0
String userCity;
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String city = address.getCity();
if (city != null) {
userCity = city.toUpperCase();
} else {
userCity = "UNKNOWN";
}
} else {
userCity = "UNKNOWN";
}
} else {
userCity = "UNKNOWN";
}
Named Arguments
Some things just require a lot of data. Do not apply the Java way of inputting it into a function or class constructor:
// Avoid ❌ - Hard to read, prone to errors
createUserProfile("12345", "John", "Doe", "john@example.com",
"+1234567890", LocalDate.of(1990, 1, 1),
Address("123 Main St", "New York", "USA", "10001"),
UserPreferences(true, false, true), UserRole.ADMIN)
Not only do we not know what exactly we input where, if the function / class constructor definition changes, this snippet above will be a serious liability.
// Good: Easy to read, definition changes are not dramatic ✅
createUserProfile(
userId = "12345",
firstName = "John",
lastName = "Doe",
email = "john@example.com",
phoneNumber = "+1234567890",
dateOfBirth = LocalDate.of(1990, 1, 1),
address = Address(
street = "123 Main St",
city = "New York",
country = "USA",
postalCode = "10001"
),
preferences = UserPreferences(
newsletterEnabled = true,
darkModeEnabled = false,
notificationsEnabled = true
),
role = UserRole.ADMIN
)
Use data classes
// Good ✅
data class User(
val id: Long,
val name: String,
val email: String,
val age: Int,
val address: Address
)
// Avoid ❌ - Extremely verbose
class User {
private val id: Long
private val name: String
private val email: String
private val age: Int
private val address: Address
constructor(id: Long, name: String, email: String, age: Int, address: Address) {
this.id = id
this.name = name
this.email = email
this.age = age
this.address = address
}
fun getId() = id
fun getName() = name
fun getEmail() = email
fun getAge() = age
fun getAddress() = address
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is User) return false
return id == other.id &&
name == other.name &&
email == other.email &&
age == other.age &&
address == other.address
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + email.hashCode()
result = 31 * result + age
result = 31 * result + address.hashCode()
return result
}
override fun toString(): String {
return "User(id=$id, name='$name', email='$email', age=$age, address=$address)"
}
}
Do not split into Java-style utils classes
Of course, you should split your code over multiple functions to avoid having 1000 line long functions, and especially regarding DRY (don't repeat yourself) reuse reusable code.
// Good ✅ - Clean, chainable extension functions
fun List<Transaction>.totalAmount() = sumOf { it.amount }
fun List<Transaction>.forPeriod(start: LocalDate, end: LocalDate) =
filter { it.date in start..end }
fun List<Transaction>.byCategory(category: Category) =
filter { it.category == category }
fun List<Transaction>.groupByMonth() =
groupBy { it.date.yearMonth }
.mapValues { it.value.totalAmount() }
fun Double.formatAsCurrency() =
"€%.2f".format(this)
// Usage looks like natural language! ✅
val result = transactions
.forPeriod(startDate, endDate)
.byCategory(Category.FOOD)
.totalAmount()
.formatAsCurrency()
val monthlyReport = transactions
.forPeriod(startDate, endDate)
.groupByMonth()
.forEach { (month, amount) ->
println("$month: ${amount.formatAsCurrency()}")
}
// Avoid ❌ - Traditional utility class approach
class TransactionAnalyzer {
fun calculateTotalAmount(transactions: List<Transaction>): Double {
return transactions.sumOf { it.amount }
}
fun filterTransactionsByPeriod(
transactions: List<Transaction>,
start: LocalDate,
end: LocalDate
): List<Transaction> {
return transactions.filter {
it.date >= start && it.date <= end
}
}
fun filterTransactionsByCategory(
transactions: List<Transaction>,
category: Category
): List<Transaction> {
return transactions.filter {
it.category == category
}
}
fun groupTransactionsByMonth(
transactions: List<Transaction>
): Map<YearMonth, Double> {
return transactions
.groupBy { YearMonth.from(it.date) }
.mapValues { calculateTotalAmount(it.value) }
}
}
class CurrencyFormatter {
fun formatAmount(amount: Double): String {
return String.format("€%.2f", amount)
}
}
// Usage is verbose and harder to read ❌
val analyzer = TransactionAnalyzer()
val formatter = CurrencyFormatter()
val filteredByPeriod = analyzer.filterTransactionsByPeriod(
transactions,
startDate,
endDate
)
val filteredByCategory = analyzer.filterTransactionsByCategory(
filteredByPeriod,
Category.FOOD
)
val total = analyzer.calculateTotalAmount(filteredByCategory)
val result = formatter.formatAmount(total)
val monthlyTransactions = analyzer.filterTransactionsByPeriod(
transactions,
startDate,
endDate
)
val monthlyTotals = analyzer.groupTransactionsByMonth(monthlyTransactions)
monthlyTotals.forEach { (month, amount) ->
println("$month: ${formatter.formatAmount(amount)}")
}
Another example with date manipulation:
// Good ✅ - Expressive extension functions
fun LocalDate.isBusinessDay() = !isWeekend() && !isHoliday()
fun LocalDate.isWeekend() = dayOfWeek in listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
fun LocalDate.isHoliday() = this in holidayList
fun LocalDate.nextBusinessDay(): LocalDate =
plusDays(1).let { if (it.isBusinessDay()) it else it.nextBusinessDay() }
fun LocalDate.previousBusinessDay(): LocalDate =
minusDays(1).let { if (it.isBusinessDay()) it else it.previousBusinessDay() }
fun LocalDate.businessDaysUntil(end: LocalDate): Int =
if (this > end) 0
else (this..end).count { it.isBusinessDay() }
// Usage is super clean! ✅
if (date.isBusinessDay()) {
processPayment()
}
val nextWorkday = date.nextBusinessDay()
val workingDays = startDate.businessDaysUntil(endDate)
// Avoid ❌ - Traditional utility class approach
class DateUtils {
fun isBusinessDay(date: LocalDate): Boolean {
return !isWeekend(date) && !isHoliday(date)
}
private fun isWeekend(date: LocalDate): Boolean {
return date.dayOfWeek == DayOfWeek.SATURDAY ||
date.dayOfWeek == DayOfWeek.SUNDAY
}
private fun isHoliday(date: LocalDate): Boolean {
return holidayList.contains(date)
}
fun findNextBusinessDay(date: LocalDate): LocalDate {
var nextDate = date.plusDays(1)
while (!isBusinessDay(nextDate)) {
nextDate = nextDate.plusDays(1)
}
return nextDate
}
fun calculateBusinessDaysBetween(
startDate: LocalDate,
endDate: LocalDate
): Int {
if (startDate > endDate) return 0
var count = 0
var currentDate = startDate
while (currentDate <= endDate) {
if (isBusinessDay(currentDate)) {
count++
}
currentDate = currentDate.plusDays(1)
}
return count
}
}
Use collection operations instead of imperative style processing
// Good ✅
val adults = users
.filter { it.age >= 18 }
.map { it.name }
// Avoid ❌ - Imperative style
val adults = mutableListOf<String>()
for (user in users) {
if (user.age >= 18) {
adults.add(user.name)
}
}
Longer example:
// Good ✅
val topActiveUsers = users
.filter { it.isActive }
.sortedByDescending { it.lastLoginDate }
.take(10)
.map { UserDTO(it.id, it.name, it.email) }
val userStatistics = users
.groupBy { it.country }
.mapValues { (_, users) ->
users.fold(UserStats()) { stats, user ->
stats.copy(
totalUsers = stats.totalUsers + 1,
activeUsers = stats.activeUsers + if (user.isActive) 1 else 0,
averageAge = users.map { it.age }.average(),
totalPurchases = stats.totalPurchases + user.purchases.size
)
}
}
// Avoid ❌ - Imperative style
val topActiveUsers = mutableListOf<UserDTO>()
val activeUsers = mutableListOf<User>()
for (user in users) {
if (user.isActive) {
activeUsers.add(user)
}
}
activeUsers.sortWith(compareByDescending { it.lastLoginDate })
var count = 0
for (user in activeUsers) {
if (count < 10) {
topActiveUsers.add(UserDTO(user.id, user.name, user.email))
count++
} else {
break
}
}
val userStatistics = mutableMapOf<String, UserStats>()
for (user in users) {
val stats = userStatistics.getOrPut(user.country) { UserStats() }
stats.totalUsers++
if (user.isActive) {
stats.activeUsers++
}
var totalAge = 0
var userCount = 0
for (u in users.filter { it.country == user.country }) {
totalAge += u.age
userCount++
}
stats.averageAge = totalAge.toDouble() / userCount
stats.totalPurchases += user.purchases.size
}
Use When Expression Instead of (multiple) If-Else
// Good ✅
val result = when (value) {
1 -> "One"
2 -> "Two"
else -> "Other"
}
// Avoid ❌ - Hard to read
val result = if (value == 1) {
"One"
} else if (value == 2) {
"Two"
} else {
"Other"
}
Do not create multiple unnecessary overloads, use default arguments instead
// Good ✅
fun createUser(
name: String,
email: String,
isActive: Boolean = true
)
// Avoid ❌ - Multiple overloads
fun createUser(name: String, email: String)
fun createUser(name: String, email: String, isActive: Boolean)
Use scope functions
// Good ✅ - Clean, nested scope functions with clear intent
class OrderProcessor {
fun processOrder(rawOrder: RawOrder) = Order().apply {
id = generateOrderId()
timestamp = LocalDateTime.now()
status = OrderStatus.PENDING
customer = Customer().apply {
rawOrder.customerData?.let { customerData ->
id = customerData.id
email = customerData.email.lowercase()
name = customerData.name.trim()
address = Address().apply {
customerData.address?.let { addressData ->
street = addressData.street.trim()
city = addressData.city.trim()
country = addressData.country.uppercase()
postalCode = addressData.postalCode.formatPostalCode()
validateAddress()?.let { validatedAddress ->
longitude = validatedAddress.longitude
latitude = validatedAddress.latitude
formattedAddress = validatedAddress.formatted
}
}
}
preferences = CustomerPreferences().apply {
newsletterOptIn = customerData.marketingConsent
preferredLanguage = customerData.language ?: defaultLanguage
communicationChannel = customerData.preferredChannel
specialInstructions = customerData.notes?.trim()
}
}
}
items = rawOrder.items.map { rawItem ->
OrderItem().apply {
productId = rawItem.id
quantity = rawItem.quantity.coerceAtLeast(1)
price = priceService.getPrice(rawItem.id)
customization = ItemCustomization().apply {
rawItem.customization?.let { custom ->
color = custom.color
size = custom.size
engraving = custom.text?.trim()
giftWrap = custom.isGift
if (giftWrap) {
giftMessage = custom.giftMessage?.trim()
giftWrapStyle = custom.wrapStyle ?: defaultWrapStyle
}
}
}
subtotal = price * quantity
tax = taxCalculator.calculateTax(subtotal, customer.address.country)
total = subtotal + tax
}
}
subtotal = items.sumOf { it.subtotal }
tax = items.sumOf { it.tax }
shippingCost = calculateShipping(items, customer.address)
total = subtotal + tax + shippingCost
discount = DiscountCalculator().run {
calculateDiscount(
subtotal = subtotal,
customer = customer,
items = items
)
}
finalTotal = (total - discount).coerceAtLeast(0.0)
}.also { order ->
logger.info("Created order: ${order.id}")
eventPublisher.publish(OrderCreatedEvent(order))
notificationService.notifyCustomer(order)
analyticsService.trackOrder(order)
}
}
// Avoid ❌ - Nested null checks and tons of repetitive code
class OrderProcessor {
fun processOrder(rawOrder: RawOrder): Order {
val order = Order()
order.id = generateOrderId()
order.timestamp = LocalDateTime.now()
order.status = OrderStatus.PENDING
if (rawOrder.customerData != null) {
val customer = Customer()
customer.id = rawOrder.customerData.id
customer.email = rawOrder.customerData.email.lowercase()
customer.name = rawOrder.customerData.name.trim()
if (rawOrder.customerData.address != null) {
val address = Address()
address.street = rawOrder.customerData.address.street.trim()
address.city = rawOrder.customerData.address.city.trim()
address.country = rawOrder.customerData.address.country.uppercase()
address.postalCode = rawOrder.customerData.address.postalCode.formatPostalCode()
val validatedAddress = validateAddress(address)
if (validatedAddress != null) {
address.longitude = validatedAddress.longitude
address.latitude = validatedAddress.latitude
address.formattedAddress = validatedAddress.formatted
}
customer.address = address
}
val preferences = CustomerPreferences()
preferences.newsletterOptIn = rawOrder.customerData.marketingConsent
preferences.preferredLanguage =
if (rawOrder.customerData.language != null)
rawOrder.customerData.language
else defaultLanguage
preferences.communicationChannel = rawOrder.customerData.preferredChannel
if (rawOrder.customerData.notes != null) {
preferences.specialInstructions = rawOrder.customerData.notes.trim()
}
customer.preferences = preferences
order.customer = customer
}
val orderItems = mutableListOf<OrderItem>()
for (rawItem in rawOrder.items) {
val item = OrderItem()
item.productId = rawItem.id
item.quantity =
if (rawItem.quantity < 1) 1
else rawItem.quantity
item.price = priceService.getPrice(rawItem.id)
if (rawItem.customization != null) {
val customization = ItemCustomization()
customization.color = rawItem.customization.color
customization.size = rawItem.customization.size
if (rawItem.customization.text != null) {
customization.engraving = rawItem.customization.text.trim()
}
customization.giftWrap = rawItem.customization.isGift
if (customization.giftWrap) {
if (rawItem.customization.giftMessage != null) {
customization.giftMessage = rawItem.customization.giftMessage.trim()
}
customization.giftWrapStyle =
rawItem.customization.wrapStyle ?: defaultWrapStyle
}
item.customization = customization
}
item.subtotal = item.price * item.quantity
item.tax = taxCalculator.calculateTax(item.subtotal, order.customer.address.country)
item.total = item.subtotal + item.tax
orderItems.add(item)
}
order.items = orderItems
var subtotal = 0.0
var tax = 0.0
for (item in orderItems) {
subtotal += item.subtotal
tax += item.tax
}
order.subtotal = subtotal
order.tax = tax
order.shippingCost = calculateShipping(orderItems, order.customer.address)
order.total = subtotal + tax + order.shippingCost
val discountCalculator = DiscountCalculator()
order.discount = discountCalculator.calculateDiscount(
subtotal = subtotal,
customer = order.customer,
items = orderItems
)
order.finalTotal = order.total - order.discount
if (order.finalTotal < 0) {
order.finalTotal = 0.0
}
logger.info("Created order: ${order.id}")
eventPublisher.publish(OrderCreatedEvent(order))
notificationService.notifyCustomer(order)
analyticsService.trackOrder(order)
return order
}
}
