kotlin-guide
SKILL.md
Kotlin Guide
Applies to: Kotlin 1.9+, JVM 17+, Coroutines, Android, Server-side
Core Principles
- Null Safety: Leverage the type system to eliminate null pointer exceptions at compile time
- Conciseness: Use data classes, scope functions, and destructuring to reduce boilerplate
- Coroutines for Concurrency: Structured concurrency with coroutines, not raw threads
- Immutability by Default: Prefer
valovervar, immutable collections over mutable - Interop Awareness: Write Kotlin-idiomatic code while maintaining clean Java interop boundaries
Guardrails
Version & Dependencies
- Use Kotlin 1.9+ with Gradle Kotlin DSL (
build.gradle.kts) - Target JVM 17+ for server-side, match Android minSdk for mobile
- Pin Kotlin and kotlinx library versions together (BOM alignment)
- Run
./gradlew dependenciesto audit transitive dependencies
Code Style
- Run
ktlintbefore every commit (format + check) - Run
detektfor static analysis (complexity, code smells) - Packages: lowercase, no underscores (
com.example.userservice) - Classes:
PascalCase| Functions/properties:camelCase| Constants:SCREAMING_SNAKE_CASE - Follow Kotlin Coding Conventions
Null Safety
- Never use
!!(not-null assertion) in production code - Use safe calls (
?.), elvis (?:), and smart casts instead - Nullable types only at API boundaries (deserialization, Java interop)
- Prefer
requireNotNull()orcheckNotNull()with meaningful messages over!! - Use
?.let { }for nullable transformations, not nestedifchecks
// BAD: crashes at runtime
val length = name!!.length
// GOOD: safe call with fallback
val length = name?.length ?: 0
// GOOD: explicit contract with clear error
val validName = requireNotNull(name) { "User name must not be null for ID=$id" }
Coroutines
- Always use structured concurrency (
coroutineScope,supervisorScope) - Never use
GlobalScopein production code - Use
withContext(Dispatchers.IO)for blocking I/O operations - Always handle
CancellationExceptioncorrectly (rethrow, never swallow) - Set timeouts with
withTimeoutorwithTimeoutOrNullfor external calls
// BAD: unstructured, leaks coroutines
GlobalScope.launch { fetchData() }
// GOOD: structured concurrency, respects parent lifecycle
coroutineScope {
val user = async { userService.getUser(id) }
val orders = async { orderService.getOrders(id) }
UserWithOrders(user.await(), orders.await())
}
Extension Functions
- Use extension functions to add behavior, not to bypass access control
- Keep extensions in a dedicated file (
StringExtensions.kt,DateExtensions.kt) - Do not add extensions to
Anyor overly generic types - Prefer member functions for core behavior, extensions for utility/convenience
- Document extensions with
@receiverKDoc tag when purpose is not obvious
Project Structure
myproject/
├── app/ # Application module or main entry
│ └── src/main/kotlin/
├── domain/ # Business logic, entities, use cases
│ └── src/main/kotlin/
│ └── com/example/domain/
│ ├── model/ # Data classes, sealed classes
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Business operations
├── data/ # Data layer implementations
│ └── src/main/kotlin/
│ └── com/example/data/
│ ├── repository/ # Repository implementations
│ ├── remote/ # API clients, DTOs
│ └── local/ # Database, DAOs
├── presentation/ # UI or API controllers
├── build.gradle.kts
├── settings.gradle.kts
└── gradle.properties
domain/has zero framework dependencies (pure Kotlin)data/depends ondomain/, implements repository interfacespresentation/depends ondomain/, never imports fromdata/directly- No circular module dependencies
Key Patterns
Data Classes & Value Classes
data class User(
val id: UserId,
val email: Email,
val name: String,
val role: Role = Role.VIEWER,
) {
init {
require(name.isNotBlank()) { "User name must not be blank" }
}
}
// Value classes for type-safe IDs (zero runtime overhead)
@JvmInline
value class UserId(val value: String) {
init { require(value.isNotBlank()) { "UserId must not be blank" } }
}
Sealed Classes & Interfaces
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Failure(val error: AppError) : Result<Nothing>
}
sealed class AppError(val message: String) {
data class NotFound(val resource: String, val id: String) :
AppError("$resource with ID $id not found")
data class Validation(val field: String, val reason: String) :
AppError("Validation failed for $field: $reason")
data class Unauthorized(val detail: String = "Authentication required") :
AppError(detail)
}
// Exhaustive when expressions
fun <T> Result<T>.getOrThrow(): T = when (this) {
is Result.Success -> data
is Result.Failure -> throw error.toException()
}
Scope Functions Quick Reference
| Function | Context | Returns | Use for |
|---|---|---|---|
let |
it |
Lambda result | Nullable transforms, scoped vars |
run |
this |
Lambda result | Compute value using object context |
with |
this |
Lambda result | Operate on non-null object |
apply |
this |
Object itself | Configure/build an object |
also |
it |
Object itself | Side effects (logging, events) |
// apply: configure and return the object
val request = HttpRequest.Builder().apply {
url(endpoint)
header("Authorization", "Bearer $token")
timeout(Duration.ofSeconds(30))
}.build()
// also: side effects without modifying the chain
val savedUser = userRepository.save(newUser).also { user ->
logger.info("Created user: ${user.id}")
}
Testing
Standards
- Test files:
*Test.ktinsrc/test/kotlin/(mirror source package) - Use JUnit 5 with
@Test,@Nested,@DisplayName - Use MockK for mocking (idiomatic Kotlin, supports coroutines)
- Table-driven style with
@ParameterizedTestand@MethodSource - Coverage target: >80% for business logic, >60% overall
- Use
runTestfromkotlinx-coroutines-testfor coroutine tests
Unit Test Pattern
class UserServiceTest {
private val userRepository = mockk<UserRepository>()
private val eventBus = mockk<EventBus>(relaxed = true)
private val service = UserService(userRepository, eventBus)
@Nested
@DisplayName("createUser")
inner class CreateUser {
@Test
fun `creates user with valid input`() = runTest {
coEvery { userRepository.save(any()) } returns mockUser
val result = service.createUser(validInput)
assertThat(result).isInstanceOf(Result.Success::class.java)
coVerify { userRepository.save(any()) }
coVerify { eventBus.publish(any<UserCreatedEvent>()) }
}
@Test
fun `fails with blank name`() = runTest {
val result = service.createUser(blankNameInput)
assertThat(result).isInstanceOf(Result.Failure::class.java)
coVerify(exactly = 0) { userRepository.save(any()) }
}
}
}
Parameterized Tests
companion object {
@JvmStatic
fun emailCases() = listOf(
Arguments.of("user@example.com", true),
Arguments.of("invalid-email", false),
Arguments.of("", false),
)
}
@ParameterizedTest(name = "email \"{0}\" valid={1}")
@MethodSource("emailCases")
fun `validates email format`(email: String, expected: Boolean) {
val result = runCatching { Email(email) }
assertThat(result.isSuccess).isEqualTo(expected)
}
Tooling
Essential Commands
./gradlew build # Compile + test + check
./gradlew test # Run all tests
./gradlew test --tests "*.UserServiceTest" # Specific test class
./gradlew koverReport # Coverage report
./gradlew ktlintCheck # Check formatting
./gradlew ktlintFormat # Auto-fix formatting
./gradlew detekt # Static analysis
./gradlew dependencies # Dependency tree
Gradle Kotlin DSL Configuration
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.22"
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
id("io.gitlab.arturbosch.detekt") version "1.23.4"
id("org.jetbrains.kotlinx.kover") version "0.7.5"
}
kotlin { jvmToolchain(17) }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
testImplementation(kotlin("test"))
testImplementation("io.mockk:mockk:1.13.9")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
testImplementation("org.assertj:assertj-core:3.25.1")
}
detekt {
config.setFrom("detekt.yml")
buildUponDefaultConfig = true
}
kover {
verify { rule { minBound(80) } }
}
Detekt Configuration
# detekt.yml
complexity:
LongMethod:
threshold: 50
CyclomaticComplexMethod:
threshold: 10
LargeClass:
threshold: 300
LongParameterList:
functionThreshold: 5
constructorThreshold: 8
style:
ForbiddenComment:
values:
- "TODO(?!\\(#\\d+\\))" # TODOs require issue reference
MagicNumber:
ignoreNumbers: ["-1", "0", "1", "2"]
MaxLineLength:
maxLineLength: 120
potential-bugs:
UnsafeCast:
active: true
References
For detailed patterns and examples, see:
- references/patterns.md -- Coroutine patterns, sealed class hierarchies, DSL builders, Flow patterns
External References
Weekly Installs
6
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
Mar 1, 2026
Security Audits
Installed on
opencode6
gemini-cli6
github-copilot6
codex6
kimi-cli6
amp6