CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

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 Kotlin coroutines", "fix Kotlin style", "set up Detekt", "configure ktlint", "add static analysis to a Kotlin project", "set up Kotlin linting (Detekt, ktlint)", 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). Covers the language and stdlib only; the one framework-specific part is an optional Android/Compose lint section. For Android app code (Compose UI, lifecycle, platform APIs) use google-app-development alongside it.

74

Quality

93%

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. The lint configuration is target-neutral; Android/Compose settings live in one clearly marked section of references/static-analysis.md and apply only when the project uses them.

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.md — build.gradle.kts, multi-module, compiler options, testing setup
  • references/static-analysis.md — Detekt (setup, detekt.yml, rule sets, custom rules, baseline), ktlint (setup, .editorconfig, standard rules, Spotless), Detekt vs ktlint comparison, multi-module convention plugin, pre-commit hooks, Android/Compose additions (optional)

Code Style

  • Short functions — aim for one responsibility and ~20 lines. If a block needs a comment to explain it, extract it into a well-named function. The ~20 is a target, not the lint limit: the Detekt config in references/static-analysis.md fails LongMethod at 60 lines.
  • 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)

Use val properties — change state with copy(). 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)

// Recoverable failure — try/catch on the narrowest type you can handle
val port = try {
    config.getValue("port").toInt()
} catch (e: NumberFormatException) {
    8080
}

// 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:

  • Use require() for argument validation, check() for state validation.
  • Catch the narrowest exception that you can handle. Don't catch Throwable unless re-throwing — it hides OutOfMemoryError and cancellation.
  • Prefer sealed hierarchies over exceptions for expected outcomes.
  • runCatching catches Throwable, so it swallows Errors (OutOfMemoryError) and CancellationException alike. Use it only in non-suspending code when you want Result chaining, and recover only the types you handle, rethrowing the rest. In suspend functions use try/catch and rethrow CancellationException first.
  • 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:

  • Use structured concurrency; don't use GlobalScope — it leaks coroutines that nothing cancels.
  • coroutineScope when all must succeed; supervisorScope when partial failure is OK.
  • launch for fire-and-forget; async when you need the result.
  • Rethrow CancellationException from every catch block — swallowing it breaks structured concurrency (worked example in references/coroutines.md).
  • 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 (let, run, with, apply, also): pick by what the lambda should return and whether the object is this or it; don't nest them. Decision table in 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
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 (Detekt LongMethod fires at 60 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

Path

registry/skills/kotlin-development/SKILL.md

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.