CtrlK
BlogDocsLog inGet started
Tessl Logo

kotlin-development

This skill should be used when the user asks to "write Kotlin code", "create a Kotlin class", "set up a Kotlin project", "review Kotlin code", "refactor Kotlin", "use coroutines", "fix Kotlin style", "set up Detekt", "configure ktlint", "add static analysis", "set up code linting", or when generating any Kotlin source code. Provides modern Kotlin 2.1+ best practices covering null safety, coroutines, data modeling, error handling, idiomatic patterns, and static analysis (Detekt, ktlint). Does not cover any specific library or framework.

72

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Kotlin Development (2.1+)

Modern Kotlin best practices for writing concise, safe, and idiomatic code. Targets Kotlin 2.1+ on the JVM — language and stdlib only, no frameworks or libraries.

Reference Files

  • references/type-system.md — Generics, variance, smart casts, inline/value classes, Nothing, SAM conversions
  • references/patterns.md — Scope functions, sealed hierarchies, delegation, extensions, DSL builders, domain modeling, error handling guide
  • references/coroutines.md — Flow, StateFlow/SharedFlow, Channels, exception handling, cancellation, testing
  • references/project-structure.mdbuild.gradle.kts, multi-module, compiler options, testing setup
  • references/static-analysis.md — Detekt (setup, detekt.yml, rule sets, custom rules, baseline, Compose rules), ktlint (setup, .editorconfig, standard rules, Spotless), Detekt vs ktlint comparison, multi-module convention plugin, pre-commit hooks

Code Style

  • Short functions — target under 15 lines. If a block needs a comment to explain it, extract it into a well-named function.
  • Imports at the top — never use inline fully-qualified types (java.time.LocalDateTime) in the code body. Use import aliases for naming conflicts. No wildcard imports.

Naming Conventions

ElementConventionExample
Packagelowercasecom.example.userdata
Class / ObjectPascalCaseUserAccount, JsonParser
Function / PropertycamelCasegetUserById(), isActive
Constant (const val)UPPER_SNAKEMAX_RETRIES
Type parameterSingle uppercaseT, K, V
Enum entryUPPER_SNAKEStatus.ACTIVE
Backing property_prefixedprivate val _items
File namePascalCase.ktUserService.kt
BooleanPrefix is, has, can, shouldisValid, hasPermission

Null Safety

val name: String? = findUser()?.name   // safe call
val length = name?.length ?: 0          // elvis — default for null

NEVER use !! to silence the compiler — it crashes at runtime. Acceptable only with a provable invariant and a comment explaining why.

Safe patterns

user?.let { sendEmail(it) }                              // nullable transform
requireNotNull(id) { "id must not be null" }             // precondition + smart cast
val names: List<String> = users.mapNotNull { it.name }   // filter nulls

Type System

Kotlin infers types aggressively. Annotate explicitly for public API, when the inferred type is too broad, or when readability benefits.

// Inferred — fine for locals
val count = items.size

// Explicit — public API
fun findUser(id: String): User? { ... }

For generics (in/out variance, star projection, reified types), type aliases, smart casts, and value classes, see references/type-system.md.

Data Modeling

data class — value containers

data class User(val id: String, val name: String, val email: String)

MUST use val properties. ALWAYS prefer data classes over Map<String, Any> — maps lose type safety, autocompletion, and refactoring support.

sealed class / sealed interface — restricted hierarchies

sealed interface Result<out T> {
    data class Success<T>(val value: T) : Result<T>
    data class Failure(val error: Throwable) : Result<Nothing>
}

Enables exhaustive when. Prefer sealed interface when subtypes don't share state.

value class — zero-cost wrappers

@JvmInline
value class UserId(val value: String)

@JvmInline
value class Email(val value: String) {
    init { require(value.contains("@")) { "Invalid email: $value" } }
}

Use to prevent primitive obsession — the compiler catches swapped parameters.

Precision arithmetic

Never use Double/Float for monetary values. Use BigDecimal (construct from String, never Double) with explicit MathContext, or Long (cents) with a value class wrapper. See references/patterns.md for full patterns.

Error Handling

// Custom hierarchy
open class AppException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
class ValidationException(message: String) : AppException(message)
class NotFoundException(message: String) : AppException(message)

// runCatching for functional error handling
val name = runCatching { fetchUser(id) }.map { it.name }.getOrDefault("Unknown")

// Sealed result for expected business outcomes
sealed interface FetchResult {
    data class Found(val user: User) : FetchResult
    data object NotFound : FetchResult
    data class Error(val reason: String) : FetchResult
}

Rules:

  • ALWAYS use require() for argument validation, check() for state validation.
  • MUST catch the narrowest exception. NEVER catch Throwable unless re-throwing.
  • Prefer sealed hierarchies over exceptions for expected outcomes.
  • See references/patterns.md for the full error handling decision guide.

Coroutines Essentials

suspend fun fetchUser(id: String): User = httpClient.get("users/$id")

suspend fun loadDashboard(): Dashboard = coroutineScope {
    val user = async { fetchUser(userId) }
    val orders = async { fetchOrders(userId) }
    Dashboard(user.await(), orders.await())
}

Rules:

  • MUST use structured concurrency — NEVER use GlobalScope.
  • coroutineScope when all must succeed; supervisorScope when partial failure is OK.
  • launch for fire-and-forget; async when you need the result.
  • NEVER swallow CancellationException — ALWAYS rethrow it.
  • Use withContext(Dispatchers.IO) for blocking I/O; Dispatchers.Default for CPU work.

For Flow, Channels, exception handling, cancellation patterns, timeouts, and testing, see references/coroutines.md.

Project Structure

project-name/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/libs.versions.toml
├── src/
│   ├── main/kotlin/com/example/project/
│   └── test/kotlin/com/example/project/
└── gradlew
  • Use Gradle Kotlin DSL and version catalog (libs.versions.toml).
  • Organize by domain, not by technical role.
  • One class per file; file name matches class name.

For build.gradle.kts config, multi-module setup, compiler options, and testing, see references/project-structure.md.

Idiomatic Patterns

  • buildList / buildMap / buildString for constructing collections and strings.
  • groupBy, associateBy, partition, flatMap for collection transformations.
  • Sequence for lazy evaluation of large collections.
  • use for auto-closeable resource management.
  • Collection operations over loops: users.filter { it.age >= 18 }.map { it.name }

Scope functions quick reference

FunctionReceiverReturnsUse for
applythisreceiverObject configuration
letitlambda resultNullable transforms, scoping
runthislambda resultObject computation
alsoitreceiverSide effects
withthislambda resultGrouping calls

For the complete decision guide, see references/patterns.md.

Testable Design

  • Constructor injection — accept dependencies as constructor parameters, never instantiate internally.
  • Depend on interfaces at module boundaries.
  • Fakes over mocks — write simple in-memory implementations.
  • Pure core logic — push I/O to the edges, keep business logic in pure functions.

For full patterns, see references/patterns.md.

Quick Reference: Common Mistakes

MistakeFix
Using !! to silence nullabilityUse ?., ?:, let, or redesign to be non-null
Platform types without annotationAdd explicit nullability at Java boundaries
var in data classesUse val — copy with copy()
Catching ThrowableCatch specific exceptions; rethrow CancellationException
GlobalScope.launchUse structured concurrency
Mutable collections in public APIExpose List, not MutableList; backing property pattern
Inline fully-qualified typesImport at top; use aliases for conflicts
Map<String, Any> as data holderDefine a data class
Double for moneyBigDecimal with MathContext, or Long (cents)
Long functions (>15 lines)Extract named private functions
Hard-coded dependenciesConstructor injection; depend on interfaces
when without exhaustive checkUse sealed types or add else branch
Repository
provectus/awos-recruitment
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.