Claude-code-setup standards-kotlin
Kotlin coding standards for modern applications. Includes naming conventions, coroutines, flows, modern Kotlin 2.3.0 features, and recommended tooling.
git clone https://github.com/b33eep/claude-code-setup
T=$(mktemp -d) && git clone --depth=1 https://github.com/b33eep/claude-code-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/standards-kotlin" ~/.claude/skills/b33eep-claude-code-setup-standards-kotlin && rm -rf "$T"
skills/standards-kotlin/SKILL.mdKotlin Coding Standards
Core Principles
- Explicitness: Explicit code over implicit magic
- Readability: Readable code over clever tricks
- Null Safety: Embrace Kotlin's null safety system
- Immutability: Prefer
overval
, immutable collectionsvar - Expressiveness: Use Kotlin's expressive features (data classes, sealed classes)
- DRY: Don't Repeat Yourself - but keep it simple
General Rules
- Prefer
overval
: Immutability by defaultvar - Use data classes: For simple data holders
- Sealed classes/interfaces: For type-safe state modeling
- Early returns: Avoid deep nesting
- Descriptive names: Clear, meaningful names
- Minimal changes: Only change relevant code
- No over-engineering: Keep it simple
- Minimal comments: Self-explanatory code. Comments for "why", not "what"
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Classes | PascalCase | , |
| Interfaces | PascalCase | , |
| Functions | camelCase | , |
| Properties | camelCase | , |
| Constants | UPPER_SNAKE_CASE | , |
| Packages | lowercase.dot.separated | , |
| Files | PascalCase.kt | , |
| Test Classes | ClassNameTest | , |
| Test Functions | backtick names | |
Project Structure
Gradle Kotlin DSL Project (Recommended)
myproject/ ├── build.gradle.kts ├── settings.gradle.kts ├── gradle.properties ├── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── com/example/myapp/ │ │ │ ├── Application.kt # Main entry point │ │ │ ├── config/ │ │ │ │ └── AppConfig.kt # Configuration │ │ │ ├── domain/ │ │ │ │ └── User.kt # Domain models │ │ │ ├── repository/ │ │ │ │ └── UserRepository.kt # Data access │ │ │ ├── service/ │ │ │ │ └── UserService.kt # Business logic │ │ │ └── api/ │ │ │ └── UserController.kt # REST endpoints │ │ └── resources/ │ │ ├── application.conf │ │ └── logback.xml │ └── test/ │ ├── kotlin/ │ │ └── com/example/myapp/ │ │ ├── service/ │ │ │ └── UserServiceTest.kt │ │ └── repository/ │ │ └── UserRepositoryTest.kt │ └── resources/ │ └── application-test.conf └── README.md
Maven Project (Alternative)
myproject/ ├── pom.xml ├── src/ │ ├── main/ │ │ └── kotlin/... # Same structure as Gradle │ └── test/ │ └── kotlin/... # Same structure as Gradle └── README.md
Modern Kotlin Features
Recommended: Use Kotlin 2.3.0 (latest LTS) for new projects with K2 compiler enabled by default.
K2 Compiler (Stable since 2.0)
The K2 compiler brings significant performance improvements and faster compilation times.
Features:
- Faster compilation (up to 2x)
- Better smart casts
- Improved type inference
- Unified architecture for all platforms
Enabled by default in Kotlin 2.3.0 - no configuration needed.
Data Classes
Use data classes for immutable data holders.
// Data class - automatic equals, hashCode, toString, copy, componentN data class User( val id: String, val name: String, val email: String, val age: Int ) // Usage val user = User("1", "John Doe", "john@example.com", 30) // Copy with changes val updatedUser = user.copy(age = 31) // Destructuring val (id, name, email, age) = user println("User: $name ($email)")
Sealed Classes/Interfaces (Exhaustive When)
Use sealed classes for type-safe state modeling with exhaustive
when expressions.
// Sealed interface for result types sealed interface 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> } // Exhaustive when - compiler ensures all cases are handled fun <T> handleResult(result: Result<T>) { when (result) { is Result.Success -> println("Success: ${result.data}") is Result.Error -> println("Error: ${result.message}") Result.Loading -> println("Loading...") // No else needed - compiler knows all cases } } // Usage val result: Result<User> = Result.Success(user) handleResult(result)
Inline Value Classes (Zero-Cost Wrappers)
Use inline value classes for type-safe wrappers without runtime overhead.
// Inline value class - no boxing overhead @JvmInline value class UserId(val value: String) @JvmInline value class Email(val value: String) { init { require(value.contains("@")) { "Invalid email" } } } // Usage - type-safe, no runtime cost fun getUserById(id: UserId): User = TODO() fun sendEmail(email: Email): Unit = TODO() val userId = UserId("123") val email = Email("user@example.com")
Context Receivers (Experimental, 2.2+)
Context receivers allow implicit parameters for cleaner DSLs.
Enable with:
// build.gradle.kts kotlin { compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } }
Usage:
interface Logger { fun log(message: String) } // Function with context receiver context(Logger) fun processUser(user: User) { log("Processing user: ${user.name}") // ... } // Call with context val logger = object : Logger { override fun log(message: String) = println(message) } with(logger) { processUser(user) }
Explicit Backing Fields (Experimental, 2.3)
Simplifies backing property pattern - define implementation type within property scope.
Enable with:
// build.gradle.kts kotlin { compilerOptions { freeCompilerArgs.add("-Xexplicit-backing-fields") } }
Before:
private val _users = MutableStateFlow<List<User>>(emptyList()) val users: StateFlow<List<User>> = _users
After:
val users: StateFlow<List<User>> field = MutableStateFlow(emptyList())
UUID API (Experimental, 2.3)
Built-in UUID support without external dependencies.
Enable with:
@OptIn(ExperimentalUuidApi::class)
Usage:
import kotlin.uuid.Uuid import kotlin.uuid.ExperimentalUuidApi @OptIn(ExperimentalUuidApi::class) fun generateUserId(): Uuid { return Uuid.generateV4() } @OptIn(ExperimentalUuidApi::class) fun parseUserId(id: String): Uuid? { return Uuid.parseOrNull(id) } // V7 UUIDs (time-based, sortable) @OptIn(ExperimentalUuidApi::class) fun generateTimeBasedId(): Uuid { return Uuid.generateV7() }
Coroutines & Concurrency
Structured Concurrency
Always use structured concurrency - never use
GlobalScope.
import kotlinx.coroutines.* // GOOD - Structured concurrency suspend fun fetchUserData(userId: String): UserData = coroutineScope { val userDeferred = async { fetchUser(userId) } val ordersDeferred = async { fetchOrders(userId) } UserData( user = userDeferred.await(), orders = ordersDeferred.await() ) } // BAD - GlobalScope leaks fun fetchUserDataBad(userId: String) { GlobalScope.launch { // Don't use GlobalScope! // ... } }
Dispatchers
Use appropriate dispatchers for different workloads.
// Dispatchers.Default - CPU-intensive work withContext(Dispatchers.Default) { // Heavy computation processLargeDataset(data) } // Dispatchers.IO - I/O operations (network, disk) withContext(Dispatchers.IO) { // Network call apiClient.fetchData() } // Dispatchers.Main - UI updates (Android/Desktop) withContext(Dispatchers.Main) { updateUI(data) } // Dispatchers.Unconfined - Advanced use cases only
launch vs async
// launch - fire and forget, returns Job fun processInBackground() = coroutineScope { launch { // No result needed sendNotification() } } // async - returns Deferred<T>, await for result suspend fun fetchMultipleResources() = coroutineScope { val users = async { fetchUsers() } val orders = async { fetchOrders() } CombinedData( users = users.await(), orders = orders.await() ) }
Cancellation & Exception Handling
// Cancellation-aware code suspend fun longRunningTask() = coroutineScope { repeat(100) { i -> ensureActive() // Check for cancellation delay(100) println("Step $i") } } // Exception handling with supervisorScope suspend fun fetchDataSafely(): Result<Data> = try { supervisorScope { val data = async { fetchData() } Result.Success(data.await()) } } catch (e: Exception) { Result.Error("Failed to fetch data", e) } // CoroutineExceptionHandler val handler = CoroutineExceptionHandler { _, exception -> println("Caught exception: $exception") } val scope = CoroutineScope(SupervisorJob() + handler)
Flow API
StateFlow vs SharedFlow vs MutableStateFlow
// StateFlow - single value, always has current value, conflates updates class UserViewModel { private val _userName = MutableStateFlow("") val userName: StateFlow<String> = _userName.asStateFlow() fun updateUserName(name: String) { _userName.value = name } } // SharedFlow - event stream, can replay, doesn't conflate class EventBus { private val _events = MutableSharedFlow<Event>( replay = 0, // Don't replay events extraBufferCapacity = 64 ) val events: SharedFlow<Event> = _events.asSharedFlow() suspend fun emit(event: Event) { _events.emit(event) } } // Hot vs Cold flows // StateFlow/SharedFlow = Hot (emit regardless of collectors) // flow { } = Cold (only emit when collected)
Flow Operators
// Transform flows val userNames: Flow<String> = users .map { it.name } .filter { it.isNotEmpty() } .distinctUntilChanged() // Combine flows val combinedData = combine(users, orders) { users, orders -> CombinedData(users, orders) } // FlatMap variants val allOrders: Flow<Order> = users .flatMapConcat { user -> fetchOrders(user.id) } // Sequential // .flatMapMerge { user -> fetchOrders(user.id) } // Concurrent // .flatMapLatest { user -> fetchOrders(user.id) } // Cancel previous // Error handling val safeData: Flow<Data> = dataFlow .catch { e -> emit(Data.Empty) } .retry(3)
Flow Collection
// Collect in coroutine lifecycleScope.launch { userViewModel.userName.collect { name -> updateUI(name) } } // collectLatest - cancel previous collection on new emission lifecycleScope.launch { searchQuery.collectLatest { query -> // Cancelled if new query arrives val results = searchRepository.search(query) updateResults(results) } } // Single value val user = userFlow.first() // First value val user = userFlow.firstOrNull() // Or null if empty
Flow Best Practices
- Single source of truth: Expose
/StateFlow
, keepSharedFlow
/MutableStateFlow
privateMutableSharedFlow - Use
for UI: Cancel previous work on new emissionscollectLatest - Use
for cold-to-hot conversion:stateIn
val data: StateFlow<Data> = dataRepository.getData() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = Data.Empty )
Null Safety
Safe Calls, Elvis, and !! Operator
// Safe call (?.) - returns null if receiver is null val length: Int? = name?.length // Elvis operator (?:) - default value val length: Int = name?.length ?: 0 // !! operator - throws NPE if null (use sparingly!) val length: Int = name!!.length // Only if you're 100% sure it's not null // Safe cast (as?) val user: User? = obj as? User // let for null checks name?.let { n -> println("Name: $n") }
lateinit vs lazy
// lateinit - non-null var, initialized later (must be var) class MyClass { lateinit var repository: Repository fun init(repo: Repository) { repository = repo } // Check if initialized fun isInitialized() = ::repository.isInitialized } // lazy - initialized on first access (must be val) class MyClass { val expensiveValue: String by lazy { // Computed only once, on first access computeExpensiveValue() } }
Nullable Types vs Optional
// GOOD - Use nullable types (Kotlin-native) fun findUser(id: String): User? { return repository.findById(id) } // BAD - Don't use Java's Optional in Kotlin fun findUserBad(id: String): Optional<User> { // Avoid return Optional.ofNullable(repository.findById(id)) } // When interoping with Java, convert at boundary fun getUserFromJava(id: String): User? { return javaService.getUser(id).orElse(null) }
Backing Properties (Standard Pattern)
Use underscore prefix for private mutable backing properties.
// Standard pattern for exposing read-only property class UserRepository { private val _users = mutableListOf<User>() val users: List<User> get() = _users fun addUser(user: User) { _users.add(user) } } // StateFlow pattern class UserViewModel { private val _state = MutableStateFlow<UiState>(UiState.Loading) val state: StateFlow<UiState> = _state.asStateFlow() fun updateState(newState: UiState) { _state.value = newState } } // Alternative: explicit backing fields (experimental, Kotlin 2.3+) // Requires: -Xexplicit-backing-fields compiler flag val users: StateFlow<List<User>> field = MutableStateFlow(emptyList())
Scope Functions
Kotlin provides five scope functions:
let, run, with, apply, also. Choose based on context object reference and return value.
| Function | Object Reference | Return Value | Use Case |
|---|---|---|---|
| | Lambda result | Null checks, transformations |
| | Lambda result | Object configuration + computation |
| | Lambda result | Group function calls on object |
| | Context object | Object initialization |
| | Context object | Side effects |
let - Null Checks & Transformations
// Null check with let val length: Int? = name?.let { it.length } // Chain with let val result = value?.let { v -> processValue(v) }?.let { processed -> saveResult(processed) } // Execute block only if non-null user?.let { u -> println("User: ${u.name}") sendWelcomeEmail(u) } // Transform value val uppercaseName = name?.let { it.uppercase() }
apply - Object Initialization
// Object initialization (returns `this`) val user = User().apply { name = "John Doe" email = "john@example.com" age = 30 } // Configure and return val intent = Intent(context, DetailActivity::class.java).apply { putExtra("id", userId) flags = Intent.FLAG_ACTIVITY_NEW_TASK } // Builder pattern style val dialog = AlertDialog.Builder(context).apply { setTitle("Confirm") setMessage("Are you sure?") setPositiveButton("Yes") { _, _ -> confirm() } }.create()
also - Side Effects
// Side effects (returns `this`) val numbers = mutableListOf(1, 2, 3).also { println("List created with ${it.size} elements") } // Debug chain val result = processData(input) .also { println("Intermediate result: $it") } .transformData() .also { println("Final result: $it") } // Multiple operations val user = createUser().also { logUserCreation(it) sendWelcomeEmail(it) }
run - Computations
// Compute value (returns lambda result) val result = run { val x = computeX() val y = computeY() x + y } // Null check with run val result = service?.run { fetchData() processData() } // Replace multiple statements val hexString = run { val color = getColor() Integer.toHexString(color) }
with - Multiple Calls on Object
// Group operations on object (returns lambda result) val result = with(canvas) { drawCircle(centerX, centerY, radius) drawLine(startX, startY, endX, endY) drawText(text, x, y) save() } // Configure object with(sharedPreferences.edit()) { putString("username", username) putInt("age", age) apply() } // Avoid repetition val user = getUser() with(user) { println("Name: $name") println("Email: $email") println("Age: $age") }
Scope Function Selection Guide
// Use let for null checks value?.let { println(it) } // Use apply for object initialization (returns object) val config = Config().apply { timeout = 30 } // Use also for side effects (returns object) val list = getList().also { log("Size: ${it.size}") } // Use run for computations (returns result) val result = run { compute() } // Use with for grouping calls (returns result) val formatted = with(user) { "$name ($email)" }
Collections & Sequences
List vs MutableList
// Prefer immutable collections val users: List<User> = listOf(user1, user2, user3) // Mutable only when needed val mutableUsers: MutableList<User> = mutableListOf() mutableUsers.add(user4) // Read-only view of mutable collection val readOnlyView: List<User> = mutableUsers // List.of() for Java interop (immutable) val javaList = java.util.List.of(user1, user2)
Sequence for Lazy Evaluation
Use
Sequence for large collections or chained operations.
// List - eager evaluation (creates intermediate lists) val result = users .filter { it.age > 18 } .map { it.name } .take(10) // Sequence - lazy evaluation (no intermediate collections) val result = users.asSequence() .filter { it.age > 18 } .map { it.name } .take(10) .toList() // Terminal operation // Generate infinite sequence val fibonacci = generateSequence(1 to 1) { (a, b) -> b to a + b } .map { it.first } .take(10) .toList()
Collection Operations
// Transform val names = users.map { it.name } val adults = users.filter { it.age >= 18 } val groups = users.groupBy { it.department } // Reduce/Fold val totalAge = users.sumOf { it.age } val oldest = users.maxByOrNull { it.age } val names = users.fold("") { acc, user -> "$acc, ${user.name}" } // Partition val (adults, minors) = users.partition { it.age >= 18 } // Associate val userById = users.associateBy { it.id } val nameToUser = users.associateWith { it.name }
Loops on Ranges
// BAD - off-by-one prone for (i in 0..n - 1) { } for (i in 0 until n) { } // Better but verbose // GOOD - use ..< (Kotlin 1.7+) for (i in 0..<n) { } // Ranges for (i in 1..10) { } // 1 to 10 (inclusive) for (i in 1..<10) { } // 1 to 9 (exclusive end) for (i in 10 downTo 1) { } // 10 to 1 (descending) for (i in 1..10 step 2) { } // 1, 3, 5, 7, 9 // Prefer higher-order functions over loops // GOOD val adults = users.filter { it.age >= 18 } // Less idiomatic val adults = mutableListOf<User>() for (user in users) { if (user.age >= 18) { adults.add(user) } }
String Templates
Use string templates instead of concatenation.
// Simple variable - no braces needed val message = "Hello, $name!" // Expression - use braces val message = "User $name has ${children.size} children" val message = "${user.name} (${user.age} years old)" // Property access val message = "Length: ${text.length}" // Function call val message = "Result: ${compute()}" // BAD - concatenation val message = "Hello, " + name + "!" // Multiline strings with templates val json = """ { "name": "$name", "age": $age, "email": "$email" } """.trimIndent() // Multi-dollar strings (Kotlin 2.0+) - escape $ in raw strings val jsonSchema = $$""" { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "$${title ?: "unknown"}" } """
Extension Functions
Extension functions add functionality to existing classes without inheritance.
When to Use Extensions
// GOOD - Add functionality to existing type fun String.isValidEmail(): Boolean { return this.contains("@") && this.contains(".") } // Usage val email = "user@example.com" if (email.isValidEmail()) { } // GOOD - Domain-specific operations fun User.toDisplayString(): String { return "$name ($email)" } // GOOD - Collection transformations fun List<User>.activeUsers(): List<User> { return filter { it.isActive } }
Extension Best Practices
// 1. Restrict visibility (prefer private/internal) private fun String.sanitize(): String { return this.trim().lowercase() } // 2. Place extensions in same file as class (if you own it) // User.kt data class User(val name: String) fun User.greet() = "Hello, $name" // 3. Group related extensions in separate file // StringExtensions.kt fun String.isValidEmail(): Boolean { } fun String.isValidUrl(): Boolean { } // 4. Don't overuse - prefer member functions when appropriate class User { // GOOD - core behavior as member fun activate() { } } // Extension for convenience fun User.deactivate() { }
Extension Properties
// Extension property (no backing field allowed) val String.lastChar: Char? get() = this.lastOrNull() val List<Int>.median: Double get() { val sorted = this.sorted() val middle = sorted.size / 2 return if (sorted.size % 2 == 0) { (sorted[middle - 1] + sorted[middle]) / 2.0 } else { sorted[middle].toDouble() } } // Usage println("Hello".lastChar) // 'o' println(listOf(1, 3, 2).median) // 2.0
When Expressions Advanced
When with Guards (Kotlin 1.7+)
// Guard conditions in when sealed interface Status { data class Ok(val info: Info) : Status data class Error(val code: Int) : Status } fun handleStatus(status: Status): String { return when (status) { is Status.Ok if (status.info.isEmpty()) -> "no information" is Status.Ok -> "success: ${status.info}" is Status.Error if (status.code >= 500) -> "server error" is Status.Error -> "client error: ${status.code}" } } // Guard with multiple conditions fun classify(value: Int): String { return when (value) { in 0..10 if (value % 2 == 0) -> "small even" in 0..10 -> "small odd" in 11..100 -> "medium" else -> "large" } }
When as Expression
// Prefer when as expression (not statement) // GOOD val result = when (x) { 0 -> "zero" in 1..10 -> "small" else -> "large" } // BAD var result: String when (x) { 0 -> result = "zero" in 1..10 -> result = "small" else -> result = "large" }
Generics Basics
Generic Functions
// Generic function fun <T> singletonList(item: T): List<T> { return listOf(item) } val list = singletonList(42) // List<Int> val names = singletonList("Alice") // List<String> // Multiple type parameters fun <K, V> mapOf(key: K, value: V): Map<K, V> { return mapOf(key to value) } // Type constraints fun <T : Comparable<T>> max(a: T, b: T): T { return if (a > b) a else b }
Generic Classes
// Generic class class Box<T>(val value: T) { fun get(): T = value } val intBox = Box(42) val stringBox = Box("hello") // Multiple type parameters class Pair<A, B>(val first: A, val second: B) val pair = Pair(1, "one")
Variance (in, out)
// out - covariant (producer) interface Producer<out T> { fun produce(): T } // in - contravariant (consumer) interface Consumer<in T> { fun consume(item: T) } // Example class ListWrapper<out T>(private val list: List<T>) { fun get(index: Int): T = list[index] // OK - produces T // fun add(item: T) { } // Error - cannot consume T } // Covariance allows val strings: Producer<String> = object : Producer<String> { override fun produce() = "Hello" } val anys: Producer<Any> = strings // OK - String is subtype of Any
Reified Type Parameters
// inline + reified for runtime type access inline fun <reified T> isInstance(value: Any): Boolean { return value is T } val result = isInstance<String>("hello") // true val result2 = isInstance<Int>("hello") // false // Useful for type-safe casts inline fun <reified T> Any.asOrNull(): T? { return this as? T } val str: String? = obj.asOrNull<String>()
Formatting & Code Style
Follow Kotlin official coding conventions for consistent code formatting.
Indentation & Braces
// 4 spaces (not tabs) // Opening brace at line end (Java-style) if (condition) { doSomething() } // Single-line if can omit braces if (condition) return // Multi-line conditions: indent by 4 spaces if (!component.isSyncing && !hasErrors() ) { proceed() }
Whitespace Rules
// Space around binary operators val sum = a + b val result = x * y // NO space for range operator for (i in 0..10) { } // NO space for unary operators val x = -5 val y = a++ // Space after control flow keywords if (condition) { } when (x) { } for (i in list) { } // NO space before ( in calls/constructors foo(1, 2) User(name, age) // NO spaces around . or ?. user.name user?.email // Space after // // This is a comment
Trailing Commas
Encouraged at declaration/call sites (makes diffs cleaner):
// Function parameters fun process( name: String, age: Int, email: String, // trailing comma ) { } // Arguments process( name = "John", age = 30, email = "john@example.com", // trailing comma ) // Collections val list = listOf( "apple", "banana", "cherry", // trailing comma )
Expression Bodies
Prefer expression bodies for simple functions:
// GOOD - expression body fun double(x: Int) = x * 2 // BAD - block body for simple function fun double(x: Int): Int { return x * 2 } // Expression body with line break fun longFunctionName( arg1: String, arg2: String ) = processArguments(arg1, arg2)
Named Arguments
Use named arguments for clarity:
// Multiple parameters of same type drawSquare(x = 10, y = 10, width = 100, height = 100) // Boolean parameters setVisibility(visible = true, animated = false) // Skip default parameters createUser(name = "John") // age has default
Testing Fundamentals
JUnit 5 + kotlin.test
import kotlin.test.* import org.junit.jupiter.api.* class UserServiceTest { private lateinit var userService: UserService private lateinit var repository: UserRepository @BeforeEach fun setup() { repository = InMemoryUserRepository() userService = UserService(repository) } @Test fun `should return user when id exists`() { // Given val user = User("1", "John", "john@example.com", 30) repository.save(user) // When val result = userService.findById("1") // Then assertNotNull(result) assertEquals("John", result.name) } @Test fun `should return null when user not found`() { // When val result = userService.findById("999") // Then assertNull(result) } @ParameterizedTest @ValueSource(strings = ["", " ", "\t"]) fun `should throw when id is blank`(id: String) { assertFailsWith<IllegalArgumentException> { userService.findById(id) } } }
Mockk for Mocking
import io.mockk.* import kotlin.test.* class UserServiceTest { private val repository = mockk<UserRepository>() private val userService = UserService(repository) @Test fun `should call repository when finding user`() { // Given val user = User("1", "John", "john@example.com", 30) every { repository.findById("1") } returns user // When val result = userService.findById("1") // Then verify { repository.findById("1") } assertEquals("John", result?.name) } @Test fun `should handle repository exception`() { // Given every { repository.findById(any()) } throws RuntimeException("DB error") // When/Then assertFailsWith<RuntimeException> { userService.findById("1") } } }
Coroutines Testing
import kotlinx.coroutines.test.* import kotlin.test.* class UserViewModelTest { @Test fun `should fetch user on init`() = runTest { // Given val repository = FakeUserRepository() val viewModel = UserViewModel(repository) // When advanceUntilIdle() // Then assertEquals(UserState.Success(user), viewModel.state.value) } @Test fun `should emit loading state while fetching`() = runTest { // Given val repository = SlowUserRepository() val viewModel = UserViewModel(repository) val states = mutableListOf<UserState>() // Collect states backgroundScope.launch { viewModel.state.toList(states) } // When advanceUntilIdle() // Then assertEquals( listOf(UserState.Loading, UserState.Success(user)), states ) } }
Build Tool Awareness
Gradle Kotlin DSL (build.gradle.kts) - Recommended
// build.gradle.kts plugins { kotlin("jvm") version "2.3.10" application } group = "com.example" version = "1.0.0" repositories { mavenCentral() } dependencies { // Kotlin standard library (automatically added in 2.3+) // implementation(kotlin("stdlib")) // Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") // Testing testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("io.mockk:mockk:1.13.8") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") } kotlin { jvmToolchain(21) // Use Java 21 compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") // Enable experimental features freeCompilerArgs.add("-Xexplicit-backing-fields") } } tasks.test { useJUnitPlatform() } application { mainClass.set("com.example.ApplicationKt") }
Gradle Dependencies: implementation vs api
dependencies { // implementation - internal dependency, not exposed to consumers implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") // api - exposed to consumers (only in library modules) api("com.example:shared-models:1.0.0") // compileOnly - needed only at compile time compileOnly("org.jetbrains:annotations:24.0.0") // runtimeOnly - needed only at runtime runtimeOnly("com.h2database:h2:2.2.220") }
Maven (pom.xml) - Alternative
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>myapp</artifactId> <version>1.0.0</version> <properties> <kotlin.version>2.3.10</kotlin.version> <kotlin.compiler.jvmTarget>21</kotlin.compiler.jvmTarget> </properties> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>org.jetbrains.kotlinx</groupId> <artifactId>kotlinx-coroutines-core</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-test-junit5</artifactId> <version>${kotlin.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <sourceDirectory>src/main/kotlin</sourceDirectory> <testSourceDirectory>src/test/kotlin</testSourceDirectory> <plugins> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> </plugin> </plugins> </build> </project>
Recommended Tooling
| Tool | Purpose |
|---|---|
| Build automation (Kotlin DSL recommended) |
/ | Testing framework |
| Mocking library (Kotlin-native) |
| Code formatting & linting |
| Static code analysis, code smells |
| Coroutines testing utilities |
| JSON/protobuf serialization |
| Integration testing with Docker |
| Alternative testing framework (BDD-style) |
ktlint Usage
# Install ktlint brew install ktlint # macOS # Or download from: https://github.com/pinterest/ktlint # Format code ktlint -F "src/**/*.kt" # Check code ktlint "src/**/*.kt" # Gradle plugin # build.gradle.kts plugins { id("org.jlleitschuh.gradle.ktlint") version "12.0.3" }
detekt Usage
// build.gradle.kts plugins { id("io.gitlab.arturbosch.detekt") version "1.23.0" } detekt { buildUponDefaultConfig = true allRules = false config.setFrom(files("$projectDir/config/detekt.yml")) } dependencies { detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.0") }
Production Best Practices
- Immutability by default - Use
overval
, immutable collectionsvar - Null safety - Embrace nullable types, avoid
operator!! - Sealed classes for state - Type-safe state modeling with exhaustive
when - Structured concurrency - Always use
, nevercoroutineScopeGlobalScope - Flow for streams - Use
/StateFlow
for reactive stateSharedFlow - Data classes for DTOs - Automatic
,equals
,hashCode
,toStringcopy - Inline value classes - Type-safe wrappers without runtime cost
- Explicit over implicit - Clear, readable code over clever tricks
- Early returns - Reduce nesting, improve readability
- Descriptive naming - Functions and properties should explain intent
- Prefer expressions - Use
,when
as expressions when possibleif - Use extension functions - Add functionality without inheritance
- Coroutine cancellation - Always handle cancellation properly
- Test coroutines properly - Use
andrunTestTestDispatcher - ktlint + detekt - Automate code quality checks in CI
Comments - Less is More
// BAD - redundant comment // Get user from repository val user = repository.findById(id) // GOOD - self-explanatory code, no comment needed val user = repository.findById(id) // GOOD - comment explains WHY (not obvious) // Rate limit: API allows max 100 requests per minute per client rateLimiter.acquire() // GOOD - KDoc for public API /** * Fetches user by ID. Returns null if user not found. * * @param id User ID (non-blank) * @throws IllegalArgumentException if id is blank */ fun findUserById(id: String): User?
References
- Kotlin Language Spec: https://kotlinlang.org/spec/
- Kotlin Coding Conventions: https://kotlinlang.org/docs/coding-conventions.html
- Kotlin Coroutines Guide: https://kotlinlang.org/docs/coroutines-guide.html
- Effective Kotlin by Marcin Moskała
- Kotlin in Action by Dmitry Jemerov & Svetlana Isakova