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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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:
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:
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:
// 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:
// 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:
// 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
- Kotlin Official Idioms Documentation
- Effective Kotlin Book by Marcin Moskała
- Kotlin Koans - Interactive Learning
Exercises
- Refactoring Practice: Take a Java class and convert it to idiomatic Kotlin code.
- Collection Processing: Write a function that processes a list of numbers to calculate statistics (min, max, average) using functional operations.
- Extension Utility: Create useful extension functions for the
List<String>
class (e.g., finding the longest string, sorting by length). - Null Safety: Write a function that safely extracts information from a complex object graph with potentially null values.
- 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! :)