Kotlin Patterns & Idioms
Data Classes & Destructuring
data class User(
val id: Long,
val name: String,
val email: String,
val role: Role = Role.USER
)
// Destructuring
val (id, name, email) = user
// Copy with changes (immutable update)
val updated = user.copy(name = "Jane", role = Role.ADMIN)
Sealed Classes & When
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val cause: Throwable? = null) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
fun <T> Result<T>.getOrElse(default: T): T = when (this) {
is Result.Success -> data
is Result.Error -> default
Result.Loading -> default
}
// Exhaustive when (no else needed for sealed)
fun handleResult(result: Result<User>) = when (result) {
is Result.Success -> println("Got user: ${result.data.name}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
}
Extension Functions
fun String.isValidEmail(): Boolean =
matches(Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"))
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) get(1) else null
fun BigDecimal.format(currency: Currency = Currency.getInstance("USD")): String =
NumberFormat.getCurrencyInstance().apply { this.currency = currency }.format(this)
// Extension on existing types
fun LocalDate.isWeekend(): Boolean = dayOfWeek in setOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
Scope Functions
// let — transform nullable or create a scope
user?.let { println("User: ${it.name}") }
val length = name?.let { it.trim().length } ?: 0
// apply — configure object, returns receiver
val user = User().apply {
name = "Alice"
email = "alice@example.com"
}
// run — execute block, return result
val summary = user.run {
"${name} (${email})"
}
// also — side effects, returns receiver
repository.save(user).also { log.info("Saved user ${it.id}") }
// with — call multiple methods, no receiver
val result = with(stringBuilder) {
append("Hello")
append(", World")
toString()
}
Companion Objects & Factory Methods
class ApiClient private constructor(
private val baseUrl: String,
private val apiKey: String
) {
companion object {
fun create(baseUrl: String, apiKey: String): ApiClient {
require(baseUrl.isNotBlank()) { "baseUrl cannot be blank" }
require(apiKey.length >= 32) { "apiKey must be at least 32 chars" }
return ApiClient(baseUrl, apiKey)
}
val DEFAULT = create("https://api.example.com", System.getenv("API_KEY") ?: "")
}
}
Operator Overloading
data class Money(val amount: BigDecimal, val currency: String) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "Currency mismatch" }
return copy(amount = amount + other.amount)
}
operator fun times(factor: Int): Money = copy(amount = amount * factor.toBigDecimal())
operator fun compareTo(other: Money): Int = amount.compareTo(other.amount)
}
val total = Money(10.bd, "USD") + Money(5.bd, "USD") // Money(15, USD)
Type-Safe DSL Builder
@DslMarker annotation class EmailDsl
@EmailDsl
class EmailBuilder {
var to: String = ""
var subject: String = ""
private val body = StringBuilder()
fun body(block: StringBuilder.() -> Unit) { body.block() }
fun build() = Email(to, subject, body.toString())
}
fun email(block: EmailBuilder.() -> Unit): Email = EmailBuilder().apply(block).build()
// Usage
val msg = email {
to = "user@example.com"
subject = "Welcome"
body { append("Hello, World!") }
}
Delegation
// Property delegation
class UserPreferences(private val prefs: SharedPreferences) {
var darkMode: Boolean by prefs.boolean("dark_mode", default = false)
var language: String by prefs.string("language", default = "en")
}
// Interface delegation
class LoggingRepository(
private val delegate: UserRepository
) : UserRepository by delegate {
override fun save(user: User): User {
log.info("Saving user ${user.id}")
return delegate.save(user).also { log.info("Saved user ${it.id}") }
}
}
Inline Functions & Reified Types
inline fun <reified T> String.fromJson(): T = objectMapper.readValue(this, T::class.java)
inline fun <reified T : ViewModel> Fragment.viewModel(): T =
ViewModelProvider(this)[T::class.java]
// measureTime — stdlib
val (result, duration) = measureTimedValue {
expensiveOperation()
}
Key Rules
- Prefer
val over var; immutability is the default
- Use
data class for value objects; sealed class for algebraic types
- Scope functions:
let for null checks, apply for builder patterns, also for side effects
- Avoid
!! (non-null assertion) — use ?: throw, requireNotNull, or safe navigation
companion object is the Kotlin equivalent of Java static — don't abuse it for unrelated utilities