Idiomatic Kotlin patterns: sealed classes, data classes, extension functions, coroutines, Flow, DSL builders, value classes, and functional idioms. Use when writing or reviewing Kotlin code.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
String or Int primitives with value classes to enforce type safety at compile timeval and copy() for immutable updates// Avoid
var userId = ""
userId = input.id
// Prefer
val userId = input.id
// Safe call + Elvis
val name = user?.profile?.displayName ?: "Anonymous"
// let for null-conditional blocks
user?.let { u ->
analyticsService.track(u.id, "login")
sessionManager.create(u)
}
// also for side effects (returns receiver)
val user = createUser(request).also {
log.info("Created user: ${it.id}")
}
// run for scoped transformations
val summary = user.run {
"${name} (${email})"
}
sealed class AuthResult {
data class Success(val token: String, val user: User) : AuthResult()
data class InvalidCredentials(val attemptsRemaining: Int) : AuthResult()
data object AccountLocked : AuthResult()
data class Error(val cause: Throwable) : AuthResult()
}
fun handleAuth(result: AuthResult) = when (result) {
is AuthResult.Success -> redirect("/dashboard")
is AuthResult.InvalidCredentials -> showRetryForm(result.attemptsRemaining)
AuthResult.AccountLocked -> showLockedPage()
is AuthResult.Error -> showError(result.cause)
// Compiler guarantees exhaustiveness — no else needed
}
@JvmInline
value class UserId(val value: String) {
init { require(value.isNotBlank()) { "UserId cannot be blank" } }
}
@JvmInline
value class Email(val value: String) {
init { require(value.contains('@')) { "Invalid email: $value" } }
}
// Now these won't compile mixed up:
fun findUser(id: UserId): User? // NOT String
fun sendEmail(to: Email): Unit // NOT String
data class Money(val amount: BigDecimal, val currency: Currency) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "Currency mismatch" }
return copy(amount = amount + other.amount)
}
companion object {
fun of(amount: String, currency: String) =
Money(BigDecimal(amount), Currency.getInstance(currency))
}
}
// Parallel execution — both run concurrently, both must succeed
suspend fun loadDashboard(userId: UserId): Dashboard =
coroutineScope {
val user = async { userRepo.findById(userId) }
val stats = async { statsRepo.getForUser(userId) }
Dashboard(user.await()!!, stats.await())
}
// Sequential with timeout
suspend fun fetchWithTimeout(): Data = withTimeout(5000) {
apiClient.fetch()
}
// Producer
fun liveUpdates(id: String): Flow<Update> = flow {
while (true) {
emit(repo.getLatest(id))
delay(5000)
}
}.flowOn(Dispatchers.IO)
// Consumer with operators
viewModelScope.launch {
liveUpdates(itemId)
.filter { it.isRelevant }
.map { it.toUiModel() }
.catch { e -> _error.emit(e.message) }
.collect { update -> _state.emit(update) }
}
// CPU-intensive work
withContext(Dispatchers.Default) { heavyComputation() }
// I/O (DB, network, file)
withContext(Dispatchers.IO) { database.query() }
// Main thread (UI)
withContext(Dispatchers.Main) { updateUi() }
// String utilities
fun String.toSlug(): String =
lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-')
fun String.truncate(maxLength: Int, suffix: String = "..."): String =
if (length <= maxLength) this
else take(maxLength - suffix.length) + suffix
// Collection utilities
fun <T> List<T>.second(): T = this[1]
fun <K, V> Map<K, V>.getOrThrow(key: K): V =
get(key) ?: error("Key not found: $key")
// The DSL
fun emailMessage(block: EmailBuilder.() -> Unit): EmailMessage =
EmailBuilder().apply(block).build()
@EmailDsl
class EmailBuilder {
var from: String = ""
var to: MutableList<String> = mutableListOf()
var subject: String = ""
private var body: String = ""
fun to(address: String) { to.add(address) }
fun body(content: String) { body = content }
internal fun build() = EmailMessage(from, to.toList(), subject, body)
}
@DslMarker
annotation class EmailDsl
// Usage
val message = emailMessage {
from = "sender@example.com"
to("recipient@example.com")
subject = "Hello"
body("Hi there!")
}
// fold for aggregation
val total = orders.fold(Money.ZERO) { acc, order -> acc + order.total }
// partition
val (active, inactive) = users.partition { it.isActive }
// groupBy + mapValues
val byDepartment: Map<Department, List<Employee>> =
employees.groupBy { it.department }
// flatMap for monad chaining
fun findUserOrders(userId: UserId): List<Order> =
userRepo.findById(userId)
?.let { orderRepo.findByUser(it) }
?: emptyList()
Wrong:
val name = user!!.profile!!.displayName!! // NullPointerException at runtime with no context
Correct:
val name = user?.profile?.displayName ?: "Anonymous"
// Or, when null is a programming error:
val name = requireNotNull(user?.profile?.displayName) { "displayName must not be null for registered users" }
Why: !! throws an opaque NullPointerException; safe calls with ?: or requireNotNull provide explicit fallbacks and meaningful error messages.
Wrong:
data class Order(var status: OrderStatus, var total: Money)
fun confirm(order: Order): Order {
order.status = OrderStatus.CONFIRMED // Mutates the original — hidden side effect
return order
}
Correct:
data class Order(val status: OrderStatus, val total: Money)
fun confirm(order: Order): Order =
order.copy(status = OrderStatus.CONFIRMED) // Returns a new instance
Why: Mutating data class fields in-place creates hidden side effects that are invisible at the call site; copy() makes state transitions explicit and safe to share.
Wrong:
fun handleAuth(result: AuthResult) = when (result) {
is AuthResult.Success -> redirect("/dashboard")
else -> showError() // New subtypes silently fall through to this branch
}
Correct:
fun handleAuth(result: AuthResult) = when (result) {
is AuthResult.Success -> redirect("/dashboard")
is AuthResult.InvalidCredentials -> showRetryForm(result.attemptsRemaining)
AuthResult.AccountLocked -> showLockedPage()
is AuthResult.Error -> showError(result.cause)
// No else — compiler enforces exhaustiveness
}
Why: An else branch defeats the exhaustiveness check that sealed classes provide; adding a new subtype compiles silently but takes the wrong code path at runtime.
Wrong:
fun loadDashboard(userId: UserId) {
GlobalScope.launch { // Unstructured — leaks if caller is cancelled
val user = userRepo.findById(userId)
_state.value = UiState.Success(user)
}
}
Correct:
fun loadDashboard(userId: UserId) {
viewModelScope.launch { // Tied to ViewModel lifecycle — cancelled automatically
val user = userRepo.findById(userId)
_state.value = UiState.Success(user)
}
}
Why: GlobalScope coroutines are not tied to any lifecycle and continue running even after the owning component is destroyed, causing memory leaks and stale state updates.
Wrong:
fun findUser(id: String): User? // id could be an email, a UUID, or anything else
fun sendWelcome(email: String): Unit // Nothing stops passing an id as the email
Correct:
@JvmInline value class UserId(val value: String)
@JvmInline value class Email(val value: String) {
init { require(value.contains('@')) { "Invalid email: $value" } }
}
fun findUser(id: UserId): User?
fun sendWelcome(to: Email): Unit
Why: Raw String parameters allow callers to accidentally pass the wrong kind of string; value classes are erased at runtime (no boxing overhead) and make type errors compile-time failures.
val used instead of var wherever possible!! — null safety handled with ?., ?:, let, requireNotNullcopy for updates)coroutineScope for structured concurrencyCancellationException not swallowed<Type>Extensions.kt files