Skip to main content

Kotlin Idiomatic Code

Writing idiomatic Kotlin code means utilizing the language's features to create more readable, concise, and maintainable code. Unlike simply making your code work, idiomatic code follows the conventions and patterns that the language was designed to support.

In this guide, we'll explore how to transform regular code into idiomatic Kotlin code, allowing you to take full advantage of what Kotlin has to offer.

Introduction to Idiomatic Kotlin

Idiomatic code is code written in a way that feels natural for the language. It leverages language-specific features and follows established conventions to make your code:

  • More readable for other Kotlin developers
  • More concise while maintaining clarity
  • Less prone to errors
  • Easier to maintain and update

As you transition from other languages like Java to Kotlin, it's essential to adopt Kotlin's idioms rather than bringing patterns from other languages that might not fit well in Kotlin's ecosystem.

Key Kotlin Idioms

1. Using val vs var

One of the most fundamental Kotlin idioms is preferring immutability whenever possible.

kotlin
// Non-idiomatic (mutable variables when not needed)
var name = "John"
var greeting = "Hello, " + name

// Idiomatic Kotlin (immutable by default)
val name = "John"
val greeting = "Hello, $name"

Using val (read-only properties) by default helps prevent accidental modifications and makes your code easier to reason about.

2. String Templates

Kotlin provides elegant string templates instead of string concatenation.

kotlin
// Non-idiomatic (Java-style concatenation)
val message = "User " + user.name + " is " + user.age + " years old";

// Idiomatic Kotlin (string templates)
val message = "User ${user.name} is ${user.age} years old"

// For simple variables, you can omit the braces
val greeting = "Hello, $name"

3. Expression Functions

In Kotlin, you can write concise single-expression functions without curly braces.

kotlin
// Non-idiomatic
fun double(x: Int): Int {
return x * 2
}

// Idiomatic Kotlin
fun double(x: Int): Int = x * 2

// Return type can often be inferred
fun double(x: Int) = x * 2

4. Smart Casts

Kotlin's smart casts eliminate the need for explicit casting after a type check.

kotlin
// Non-idiomatic (Java-style casting)
if (obj instanceof String) {
String str = (String) obj;
// use str
}

// Idiomatic Kotlin
if (obj is String) {
// obj is automatically cast to String
println(obj.length)
}

5. When Expression

Kotlin's when expression is a powerful replacement for switch statements and complex if-else chains.

kotlin
// Non-idiomatic (if-else chain)
fun describe(number: Int): String {
if (number == 0) {
return "Zero"
} else if (number > 0) {
return "Positive"
} else {
return "Negative"
}
}

// Idiomatic Kotlin (when expression)
fun describe(number: Int): String = when {
number == 0 -> "Zero"
number > 0 -> "Positive"
else -> "Negative"
}

6. Nullability and Safe Calls

Kotlin's null safety features prevent null pointer exceptions while keeping your code concise.

kotlin
// Non-idiomatic (null checks)
if (user != null) {
if (user.address != null) {
return user.address.city
}
}
return null

// Idiomatic Kotlin (safe calls)
return user?.address?.city

7. Elvis Operator

The Elvis operator ?: provides a concise way to handle null values.

kotlin
// Non-idiomatic
val name = if (user.name != null) user.name else "Anonymous"

// Idiomatic Kotlin
val name = user.name ?: "Anonymous"

8. Collection Operations

Kotlin's functional-style collection operations make list processing more readable.

kotlin
// Non-idiomatic (imperative style)
val result = ArrayList<Int>()
for (number in numbers) {
if (number % 2 == 0) {
result.add(number * 2)
}
}

// Idiomatic Kotlin (functional style)
val result = numbers.filter { it % 2 == 0 }.map { it * 2 }

Practical Examples

Example 1: Data Processing

Let's see how to process a list of users in an idiomatic way:

kotlin
data class User(val name: String, val age: Int, val isActive: Boolean)

// Create sample data
val users = listOf(
User("Alice", 29, true),
User("Bob", 31, false),
User("Charlie", 25, true),
User("Dave", 42, true)
)

// Non-idiomatic approach
fun findActiveUsersNamesNonIdiomatic(users: List<User>): List<String> {
val result = ArrayList<String>()
for (user in users) {
if (user.isActive && user.age < 40) {
result.add(user.name.uppercase())
}
}
return result
}

// Idiomatic Kotlin approach
fun findActiveUsersNames(users: List<User>): List<String> = users
.filter { it.isActive && it.age < 40 }
.map { it.name.uppercase() }

val activeUserNames = findActiveUsersNames(users)
println("Active users under 40: $activeUserNames")
// Output: Active users under 40: [ALICE, CHARLIE]

Example 2: Handling Nullable Properties

Here's how to handle potentially null data in an idiomatic way:

kotlin
data class Address(val street: String, val city: String, val zipCode: String)
data class Customer(val name: String, val address: Address?)

// Sample data
val customers = listOf(
Customer("Alice", Address("123 Main St", "New York", "10001")),
Customer("Bob", null),
Customer("Charlie", Address("456 Oak Ave", "San Francisco", "94102"))
)

// Non-idiomatic approach
fun getCustomerCityNonIdiomatic(customer: Customer): String {
if (customer.address == null) {
return "Unknown"
}
return customer.address.city
}

// Idiomatic Kotlin approach
fun getCustomerCity(customer: Customer): String = customer.address?.city ?: "Unknown"

customers.forEach {
println("${it.name} lives in ${getCustomerCity(it)}")
}
/*
Output:
Alice lives in New York
Bob lives in Unknown
Charlie lives in San Francisco
*/

Example 3: Singleton Pattern

Implementing the singleton pattern in Kotlin is extremely concise:

kotlin
// Non-idiomatic (Java-style singleton)
public class Logger {
private static Logger instance;

private Logger() {}

public static synchronized Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}

public void log(String message) {
System.out.println(message);
}
}

// Idiomatic Kotlin (object declaration)
object Logger {
fun log(message: String) {
println(message)
}
}

// Usage
Logger.log("This is a log message")
// Output: This is a log message

Example 4: Extension Functions

Extension functions let you add new functionality to existing classes:

kotlin
// Adding functionality to String class
fun String.isPalindrome(): Boolean {
val cleaned = this.lowercase().replace(Regex("[^a-z0-9]"), "")
return cleaned == cleaned.reversed()
}

// Usage
val testStrings = listOf("racecar", "Hello", "A man, a plan, a canal, Panama")
testStrings.forEach {
println("'$it' is palindrome: ${it.isPalindrome()}")
}
/*
Output:
'racecar' is palindrome: true
'Hello' is palindrome: false
'A man, a plan, a canal, Panama' is palindrome: true
*/

Real-World Application: Simple To-Do App

Here's how idiomatic Kotlin code might look in a simplified to-do application:

kotlin
// Data classes for tasks
data class Task(
val id: String = UUID.randomUUID().toString(),
val title: String,
val description: String = "",
val dueDate: LocalDate? = null,
val isCompleted: Boolean = false
)

// Repository using idiomatic Kotlin patterns
class TaskRepository {
private val tasks = mutableListOf<Task>()

fun addTask(task: Task) {
tasks.add(task)
}

fun removeTask(id: String) {
tasks.removeIf { it.id == id }
}

fun getTaskById(id: String): Task? = tasks.find { it.id == id }

fun getAllTasks(): List<Task> = tasks.toList()

fun getPendingTasks(): List<Task> = tasks.filter { !it.isCompleted }

fun getCompletedTasks(): List<Task> = tasks.filter { it.isCompleted }

fun getTasksDueToday(): List<Task> = tasks.filter {
it.dueDate?.equals(LocalDate.now()) ?: false
}

fun markAsCompleted(id: String) {
val index = tasks.indexOfFirst { it.id == id }
if (index != -1) {
tasks[index] = tasks[index].copy(isCompleted = true)
}
}
}

// Example usage
fun main() {
val repository = TaskRepository()

// Add some tasks
repository.addTask(Task(title = "Learn Kotlin", dueDate = LocalDate.now()))
repository.addTask(Task(title = "Go grocery shopping", dueDate = LocalDate.now().plusDays(1)))
repository.addTask(Task(title = "Prepare presentation"))

// Use functional operations to display tasks
println("All tasks:")
repository.getAllTasks().forEach {
println("- ${it.title}${it.dueDate?.let { date -> " (Due: $date)" } ?: ""}")
}

// Mark a task as completed
repository.getAllTasks().firstOrNull()?.let {
repository.markAsCompleted(it.id)
println("\nMarked '${it.title}' as completed")
}

// Show pending vs completed
println("\nPending tasks: ${repository.getPendingTasks().size}")
println("Completed tasks: ${repository.getCompletedTasks().size}")

// Tasks due today using extension function
println("\nTasks due today:")
repository.getTasksDueToday().takeIf { it.isNotEmpty() }?.forEach {
println("- ${it.title}")
} ?: println("No tasks due today")
}

/*
Output:
All tasks:
- Learn Kotlin (Due: 2023-08-10)
- Go grocery shopping (Due: 2023-08-11)
- Prepare presentation

Marked 'Learn Kotlin' as completed

Pending tasks: 2
Completed tasks: 1

Tasks due today:
- Learn Kotlin
*/

Summary

Idiomatic Kotlin code takes full advantage of the language's modern features to create more readable, concise, and maintainable code. The key idioms include:

  • Using immutable variables (val) by default
  • String templates for string interpolation
  • Expression functions for concise code
  • Smart casts to avoid explicit type casting
  • The when expression for complex conditionals
  • Null safety with safe calls and the Elvis operator
  • Functional-style collection operations
  • Extension functions to extend existing classes
  • Data classes for model objects
  • Object declarations for singletons

By adopting these idioms, your Kotlin code becomes not only more efficient but also more aligned with the language's design philosophy, making it easier for other Kotlin developers to understand and contribute to your codebase.

Additional Resources

Exercises

  1. Refactoring Practice: Take a Java class and convert it to idiomatic Kotlin code.
  2. Collection Processing: Write a function that processes a list of numbers to calculate statistics (min, max, average) using functional operations.
  3. Extension Utility: Create useful extension functions for the List<String> class (e.g., finding the longest string, sorting by length).
  4. Null Safety: Write a function that safely extracts information from a complex object graph with potentially null values.
  5. DSL Practice: Create a simple domain-specific language (DSL) for building HTML elements in Kotlin.

Remember, the best way to learn idiomatic Kotlin is through practice. Try to refactor your existing code to use these idioms and read well-written Kotlin codebases to internalize the patterns.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)