--- name: standards-kotlin description: Kotlin coding standards for modern applications. Includes naming conventions, coroutines, flows, modern Kotlin 2.3.0 features, and recommended tooling. type: context applies_to: [kotlin, gradle, maven, junit, kotest, kotlinx-coroutines, ktor, spring, mockk, testcontainers] file_extensions: [".kt", ".kts"] --- # Kotlin Coding Standards ## Core Principles 1. **Explicitness**: Explicit code over implicit magic 2. **Readability**: Readable code over clever tricks 3. **Null Safety**: Embrace Kotlin's null safety system 4. **Immutability**: Prefer `val` over `var`, immutable collections 5. **Expressiveness**: Use Kotlin's expressive features (data classes, sealed classes) 6. **DRY**: Don't Repeat Yourself - but keep it simple ## General Rules - **Prefer `val` over `var`**: Immutability by default - **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 | `UserService`, `OrderRepository` | | Interfaces | PascalCase | `UserRepository`, `PaymentProcessor` | | Functions | camelCase | `getUserById`, `calculateTotal` | | Properties | camelCase | `firstName`, `totalAmount` | | Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT`, `DEFAULT_TIMEOUT` | | Packages | lowercase.dot.separated | `com.example.service`, `com.example.repository` | | Files | PascalCase.kt | `UserService.kt`, `OrderRepository.kt` | | Test Classes | ClassNameTest | `UserServiceTest`, `OrderRepositoryTest` | | Test Functions | backtick names | `` `should return user when id exists` `` | ## 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. ```kotlin // 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. ```kotlin // Sealed interface for result types sealed interface Result { data class Success(val data: T) : Result data class Error(val message: String, val cause: Throwable? = null) : Result data object Loading : Result } // Exhaustive when - compiler ensures all cases are handled fun handleResult(result: Result) { 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 = Result.Success(user) handleResult(result) ``` ### Inline Value Classes (Zero-Cost Wrappers) Use inline value classes for type-safe wrappers without runtime overhead. ```kotlin // 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:** ```kotlin // build.gradle.kts kotlin { compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } ``` **Usage:** ```kotlin 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:** ```kotlin // build.gradle.kts kotlin { compilerOptions { freeCompilerArgs.add("-Xexplicit-backing-fields") } } ``` **Before:** ```kotlin private val _users = MutableStateFlow>(emptyList()) val users: StateFlow> = _users ``` **After:** ```kotlin val users: StateFlow> field = MutableStateFlow(emptyList()) ``` ### UUID API (Experimental, 2.3) Built-in UUID support without external dependencies. **Enable with:** ```kotlin @OptIn(ExperimentalUuidApi::class) ``` **Usage:** ```kotlin 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`. ```kotlin 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. ```kotlin // 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 ```kotlin // launch - fire and forget, returns Job fun processInBackground() = coroutineScope { launch { // No result needed sendNotification() } } // async - returns Deferred, 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 ```kotlin // 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 = 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 ```kotlin // StateFlow - single value, always has current value, conflates updates class UserViewModel { private val _userName = MutableStateFlow("") val userName: StateFlow = _userName.asStateFlow() fun updateUserName(name: String) { _userName.value = name } } // SharedFlow - event stream, can replay, doesn't conflate class EventBus { private val _events = MutableSharedFlow( replay = 0, // Don't replay events extraBufferCapacity = 64 ) val events: SharedFlow = _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 ```kotlin // Transform flows val userNames: Flow = 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 = 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 = dataFlow .catch { e -> emit(Data.Empty) } .retry(3) ``` ### Flow Collection ```kotlin // 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 1. **Single source of truth**: Expose `StateFlow`/`SharedFlow`, keep `MutableStateFlow`/`MutableSharedFlow` private 2. **Use `collectLatest` for UI**: Cancel previous work on new emissions 3. **Use `stateIn` for cold-to-hot conversion**: ```kotlin val data: StateFlow = dataRepository.getData() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = Data.Empty ) ``` ## Null Safety ### Safe Calls, Elvis, and !! Operator ```kotlin // 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 ```kotlin // 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 ```kotlin // 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 { // 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. ```kotlin // Standard pattern for exposing read-only property class UserRepository { private val _users = mutableListOf() val users: List get() = _users fun addUser(user: User) { _users.add(user) } } // StateFlow pattern class UserViewModel { private val _state = MutableStateFlow(UiState.Loading) val state: StateFlow = _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> 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 | |----------|-----------------|--------------|----------| | `let` | `it` | Lambda result | Null checks, transformations | | `run` | `this` | Lambda result | Object configuration + computation | | `with` | `this` | Lambda result | Group function calls on object | | `apply` | `this` | Context object | Object initialization | | `also` | `it` | Context object | Side effects | ### let - Null Checks & Transformations ```kotlin // 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 ```kotlin // 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 ```kotlin // 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 ```kotlin // 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 ```kotlin // 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 ```kotlin // 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 ```kotlin // Prefer immutable collections val users: List = listOf(user1, user2, user3) // Mutable only when needed val mutableUsers: MutableList = mutableListOf() mutableUsers.add(user4) // Read-only view of mutable collection val readOnlyView: List = 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. ```kotlin // 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 ```kotlin // 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 ```kotlin // 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..= 18 } // Less idiomatic val adults = mutableListOf() for (user in users) { if (user.age >= 18) { adults.add(user) } } ``` ## String Templates Use string templates instead of concatenation. ```kotlin // 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 ```kotlin // 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.activeUsers(): List { return filter { it.isActive } } ``` ### Extension Best Practices ```kotlin // 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 ```kotlin // Extension property (no backing field allowed) val String.lastChar: Char? get() = this.lastOrNull() val List.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+) ```kotlin // 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 ```kotlin // 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 ```kotlin // Generic function fun singletonList(item: T): List { return listOf(item) } val list = singletonList(42) // List val names = singletonList("Alice") // List // Multiple type parameters fun mapOf(key: K, value: V): Map { return mapOf(key to value) } // Type constraints fun > max(a: T, b: T): T { return if (a > b) a else b } ``` ### Generic Classes ```kotlin // Generic class class Box(val value: T) { fun get(): T = value } val intBox = Box(42) val stringBox = Box("hello") // Multiple type parameters class Pair(val first: A, val second: B) val pair = Pair(1, "one") ``` ### Variance (in, out) ```kotlin // out - covariant (producer) interface Producer { fun produce(): T } // in - contravariant (consumer) interface Consumer { fun consume(item: T) } // Example class ListWrapper(private val list: List) { fun get(index: Int): T = list[index] // OK - produces T // fun add(item: T) { } // Error - cannot consume T } // Covariance allows val strings: Producer = object : Producer { override fun produce() = "Hello" } val anys: Producer = strings // OK - String is subtype of Any ``` ### Reified Type Parameters ```kotlin // inline + reified for runtime type access inline fun isInstance(value: Any): Boolean { return value is T } val result = isInstance("hello") // true val result2 = isInstance("hello") // false // Useful for type-safe casts inline fun Any.asOrNull(): T? { return this as? T } val str: String? = obj.asOrNull() ``` ## Formatting & Code Style Follow Kotlin official coding conventions for consistent code formatting. ### Indentation & Braces ```kotlin // 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 ```kotlin // 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): ```kotlin // 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: ```kotlin // 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: ```kotlin // 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 ```kotlin 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 { userService.findById(id) } } } ``` ### Mockk for Mocking ```kotlin import io.mockk.* import kotlin.test.* class UserServiceTest { private val repository = mockk() 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 { userService.findById("1") } } } ``` ### Coroutines Testing ```kotlin 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() // 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 ```kotlin // 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 ```kotlin 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 ```xml 4.0.0 com.example myapp 1.0.0 2.3.10 21 org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} org.jetbrains.kotlinx kotlinx-coroutines-core 1.8.0 org.jetbrains.kotlin kotlin-test-junit5 ${kotlin.version} test src/main/kotlin src/test/kotlin org.jetbrains.kotlin kotlin-maven-plugin ${kotlin.version} ``` ## Recommended Tooling | Tool | Purpose | |------|---------| | `gradle` | Build automation (Kotlin DSL recommended) | | `kotlin-test` / `junit-jupiter` | Testing framework | | `mockk` | Mocking library (Kotlin-native) | | `ktlint` | Code formatting & linting | | `detekt` | Static code analysis, code smells | | `kotlinx-coroutines-test` | Coroutines testing utilities | | `kotlinx-serialization` | JSON/protobuf serialization | | `testcontainers` | Integration testing with Docker | | `kotest` | Alternative testing framework (BDD-style) | ### ktlint Usage ```bash # 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 ```kotlin // 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 1. **Immutability by default** - Use `val` over `var`, immutable collections 2. **Null safety** - Embrace nullable types, avoid `!!` operator 3. **Sealed classes for state** - Type-safe state modeling with exhaustive `when` 4. **Structured concurrency** - Always use `coroutineScope`, never `GlobalScope` 5. **Flow for streams** - Use `StateFlow`/`SharedFlow` for reactive state 6. **Data classes for DTOs** - Automatic `equals`, `hashCode`, `toString`, `copy` 7. **Inline value classes** - Type-safe wrappers without runtime cost 8. **Explicit over implicit** - Clear, readable code over clever tricks 9. **Early returns** - Reduce nesting, improve readability 10. **Descriptive naming** - Functions and properties should explain intent 11. **Prefer expressions** - Use `when`, `if` as expressions when possible 12. **Use extension functions** - Add functionality without inheritance 13. **Coroutine cancellation** - Always handle cancellation properly 14. **Test coroutines properly** - Use `runTest` and `TestDispatcher` 15. **ktlint + detekt** - Automate code quality checks in CI ## Comments - Less is More ```kotlin // 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