Claude-skill-registry lang-kotlin-dev
Foundational Kotlin patterns covering null safety, coroutines, data classes, extension functions, and Kotlin idioms. Use when writing Kotlin code or needing guidance on Kotlin development. This is the entry point for Kotlin development.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/lang-kotlin-dev" ~/.claude/skills/majiayu000-claude-skill-registry-lang-kotlin-dev && rm -rf "$T"
skills/data/lang-kotlin-dev/SKILL.mdKotlin Development Fundamentals
Overview
Kotlin is a modern, statically typed language targeting JVM, JavaScript, and native platforms. It emphasizes:
- Null safety at the type system level (nullable
vs non-nullableT?
)T - Concise syntax with data classes, smart casts, and type inference
- Coroutines for structured asynchronous programming
- Full Java interoperability for gradual migration
When to use this skill: Writing Kotlin applications, Android development, migrating from Java, or learning Kotlin idioms.
Skill Hierarchy
lang-kotlin-dev (foundational - THIS SKILL) ├── lang-kotlin-coroutines-eng (advanced concurrency patterns) ├── lang-kotlin-library-dev (library design and publishing) ├── lang-kotlin-patterns-eng (design patterns in Kotlin) └── lang-kotlin-multiplatform-dev (KMP development)
Quick Reference
| Pattern | When to Use | Key Syntax |
|---|---|---|
| Null Safety | Handling nullable types | , , , |
| Data Classes | Value objects, DTOs | |
| Sealed Classes | Restricted hierarchies | |
| Extension Functions | Add methods to existing types | |
| Scope Functions | Context-based operations | , , , , |
| Coroutines | Asynchronous programming | , , |
| Delegation | Property/class delegation | , keyword |
| Type-Safe Builders | DSL construction | Lambda with receiver |
Skill Routing
Route to specialized skills when:
- lang-kotlin-coroutines-eng: Deep coroutine patterns (Flow, channels, structured concurrency)
- lang-kotlin-library-dev: Building and publishing Kotlin libraries
- lang-kotlin-patterns-eng: Advanced design patterns and architectural patterns
- lang-kotlin-multiplatform-dev: Kotlin Multiplatform projects (KMP)
Module System
Kotlin organizes code into packages and provides visibility modifiers to control access.
1.1 Package Declarations
// Package declaration at file top package com.example.myapp.utils // Package-level functions (not in a class) fun helper(): String = "I'm package-level" // Package-level properties val VERSION = "1.0.0"
1.2 Imports
// Single import import com.example.myapp.User // Wildcard import (all public members) import com.example.myapp.utils.* // Aliased import (resolve naming conflicts) import com.example.myapp.User as AppUser import org.external.User as ExternalUser // Import extension functions import com.example.extensions.formatCurrency // Import enum entries import com.example.Status.ACTIVE import com.example.Status.INACTIVE
1.3 Visibility Modifiers
// public (default) - visible everywhere class PublicClass // internal - visible within the same module internal class ModuleClass // private - visible within the file (top-level) or class private class FilePrivateClass class Example { public val publicProp = 1 // Visible everywhere internal val internalProp = 2 // Same module protected val protectedProp = 3 // Subclasses only private val privateProp = 4 // This class only }
1.4 Object Declarations (Singletons)
// Singleton object object Logger { fun log(message: String) = println(message) } // Usage Logger.log("Hello") // Companion object (static-like members) class Factory { companion object { fun create(): Factory = Factory() const val TAG = "Factory" } } // Usage val instance = Factory.create() val tag = Factory.TAG
1.5 File Organization
// Filename: UserRepository.kt package com.example.repository // Multiple classes per file allowed (unlike Java) data class User(val id: Int, val name: String) data class UserDto(val id: Int, val displayName: String) // Extension functions in same file fun User.toDto() = UserDto(id, name) // Top-level functions fun findUserById(id: Int): User? = null
Best Practices:
- One primary class per file, named after the class
- Group related extension functions with their target type
- Use
for module-private APIs in librariesinternal
2. Null Safety
Kotlin's type system distinguishes between nullable and non-nullable types, eliminating most null pointer exceptions at compile time.
2.1 Nullable Types
// Non-nullable type (default) var name: String = "Kotlin" // name = null // Compilation error // Nullable type var nullableName: String? = "Kotlin" nullableName = null // OK // Function parameters fun greet(name: String?) { // Must handle null case }
2.2 Safe Call Operator (?.)
val length: Int? = nullableName?.length // Chaining safe calls val firstChar: Char? = nullableName?.uppercase()?.firstOrNull() // Safe calls with let nullableName?.let { name -> println("Name is $name") } // Safe call in chains data class Person(val name: String, val address: Address?) data class Address(val city: String?) val city: String? = person?.address?.city
2.3 Elvis Operator (?:)
// Provide default value for null val length: Int = nullableName?.length ?: 0 // Return early on null fun processName(name: String?): String { val trimmed = name?.trim() ?: return "No name provided" return "Hello, $trimmed" } // Throw exception on null val nonNullName = nullableName ?: throw IllegalArgumentException("Name required") // Chaining with safe calls val displayName = person?.name?.trim() ?: "Anonymous"
2.4 Not-Null Assertion (!!)
// Force unwrap - throws NPE if null val length: Int = nullableName!!.length // Use sparingly and only when certain fun processSurelyNonNull(value: String?) { // Only if you're absolutely certain val definiteValue: String = value!! } // Better: Use assertion with reasoning val config: Config = loadConfig() ?: error("Config must be initialized at startup")
Warning: Avoid
!! in production code. Use proper null handling instead.
2.5 Safe Casts (as?)
// Safe cast returns null if cast fails val stringValue: String? = value as? String // Combined with elvis operator val length: Int = (value as? String)?.length ?: 0 // Pattern matching alternative when (value) { is String -> println("String of length ${value.length}") is Int -> println("Integer: $value") else -> println("Unknown type") }
2.6 Null Safety Patterns
// Pattern 1: Early return with elvis fun processUser(user: User?): Result { val validUser = user ?: return Result.Error("User not found") return Result.Success(validUser) } // Pattern 2: Smart casts after null check fun printLength(value: String?) { if (value != null) { // value is smart-casted to String println(value.length) } } // Pattern 3: require/check for preconditions fun processPositive(value: Int?) { requireNotNull(value) { "Value must not be null" } require(value > 0) { "Value must be positive" } // value is smart-casted to Int } // Pattern 4: Safe collection operations val names: List<String?> = listOf("Alice", null, "Bob") val nonNullNames: List<String> = names.filterNotNull() val firstNonNull: String? = names.firstOrNull { it != null }
3. Data Classes
Data classes provide automatic implementation of
equals(), hashCode(), toString(), and copy().
2.1 Basic Data Classes
// Simple data class data class User( val id: Long, val name: String, val email: String ) // Automatic implementations val user1 = User(1, "Alice", "alice@example.com") val user2 = User(1, "Alice", "alice@example.com") println(user1 == user2) // true (structural equality) println(user1.toString()) // User(id=1, name=Alice, email=alice@example.com) // Destructuring val (id, name, email) = user1 println("User $name has ID $id")
2.2 Copy Method
data class User( val id: Long, val name: String, val email: String, val isActive: Boolean = true ) val user = User(1, "Alice", "alice@example.com") // Create modified copy val updatedUser = user.copy(email = "alice@newdomain.com") val inactiveUser = user.copy(isActive = false) // Copy preserves other fields println(updatedUser.name) // Alice
2.3 Data Classes with Validation
data class Email(val value: String) { init { require(value.contains("@")) { "Invalid email format" } } } data class User( val id: Long, val name: String, val email: Email ) { init { require(id > 0) { "ID must be positive" } require(name.isNotBlank()) { "Name cannot be blank" } } } // Usage val user = User(1, "Alice", Email("alice@example.com")) // val invalid = User(-1, "", Email("invalid")) // Throws IllegalArgumentException
2.4 Data Classes with Computed Properties
data class Rectangle( val width: Double, val height: Double ) { // Not part of equals/hashCode/toString val area: Double get() = width * height val perimeter: Double get() = 2 * (width + height) fun isSquare(): Boolean = width == height } val rect = Rectangle(4.0, 5.0) println(rect.area) // 20.0
2.5 Data Classes in Collections
data class User(val id: Long, val name: String) // Set automatically uses equals/hashCode val users = setOf( User(1, "Alice"), User(1, "Alice"), // Duplicate, ignored User(2, "Bob") ) println(users.size) // 2 // Map keys val userMap = mapOf( User(1, "Alice") to "admin", User(2, "Bob") to "user" ) // Finding in collections val alice = User(1, "Alice") println(userMap[alice]) // admin
4. Sealed Classes and Interfaces
Sealed classes represent restricted hierarchies where all subclasses are known at compile time.
3.1 Basic Sealed Classes
// Sealed class for result types sealed class Result<out T> { data class Success<T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() data object Loading : Result<Nothing>() } // Exhaustive when expressions fun <T> handleResult(result: Result<T>) { when (result) { is Result.Success -> println("Data: ${result.data}") is Result.Error -> println("Error: ${result.exception.message}") Result.Loading -> println("Loading...") } // No else branch needed - compiler knows all cases }
3.2 Sealed Interfaces
sealed interface NetworkState data class Connected(val speed: Int) : NetworkState data class Disconnected(val reason: String) : NetworkState data object Connecting : NetworkState fun updateUI(state: NetworkState) { when (state) { is Connected -> showConnected(state.speed) is Disconnected -> showError(state.reason) Connecting -> showLoading() } }
3.3 Nested Sealed Hierarchies
sealed class ApiResponse<out T> { data class Success<T>(val body: T) : ApiResponse<T>() sealed class Failure : ApiResponse<Nothing>() { data class HttpError(val code: Int, val message: String) : Failure() data class NetworkError(val exception: Exception) : Failure() data object Unauthorized : Failure() } data object Loading : ApiResponse<Nothing>() } fun <T> processResponse(response: ApiResponse<T>) { when (response) { is ApiResponse.Success -> handleSuccess(response.body) is ApiResponse.Failure.HttpError -> handleHttp(response.code) is ApiResponse.Failure.NetworkError -> handleNetwork(response.exception) ApiResponse.Failure.Unauthorized -> redirectToLogin() ApiResponse.Loading -> showSpinner() } }
3.4 Sealed Classes for State Machines
sealed class OrderState { data object Created : OrderState() data class Confirmed(val confirmationId: String) : OrderState() data class Shipped(val trackingNumber: String) : OrderState() data class Delivered(val signature: String) : OrderState() data class Cancelled(val reason: String) : OrderState() } class Order(var state: OrderState) { fun confirm(confirmationId: String) { state = when (state) { is OrderState.Created -> OrderState.Confirmed(confirmationId) else -> throw IllegalStateException("Can only confirm created orders") } } fun ship(trackingNumber: String) { state = when (state) { is OrderState.Confirmed -> OrderState.Shipped(trackingNumber) else -> throw IllegalStateException("Can only ship confirmed orders") } } }
3.5 Pattern: Railway-Oriented Programming
sealed class Either<out L, out R> { data class Left<L>(val value: L) : Either<L, Nothing>() data class Right<R>(val value: R) : Either<Nothing, R>() } // Extension functions for functional pipeline fun <L, R, T> Either<L, R>.map(f: (R) -> T): Either<L, T> = when (this) { is Either.Left -> this is Either.Right -> Either.Right(f(value)) } fun <L, R, T> Either<L, R>.flatMap(f: (R) -> Either<L, T>): Either<L, T> = when (this) { is Either.Left -> this is Either.Right -> f(value) } // Usage fun validateEmail(email: String): Either<String, String> = if (email.contains("@")) Either.Right(email) else Either.Left("Invalid email") fun validateLength(email: String): Either<String, String> = if (email.length >= 5) Either.Right(email) else Either.Left("Email too short") val result = validateEmail("test@example.com") .flatMap { validateLength(it) } .map { it.lowercase() }
5. Extension Functions
Extension functions add new functions to existing classes without modifying their source code.
4.1 Basic Extensions
// Extend String fun String.isPalindrome(): Boolean { val cleaned = this.lowercase().filter { it.isLetterOrDigit() } return cleaned == cleaned.reversed() } println("A man a plan a canal Panama".isPalindrome()) // true // Extend nullable types fun String?.orDefault(default: String): String = this ?: default val name: String? = null println(name.orDefault("Guest")) // Guest
4.2 Extension Properties
// Read-only extension property val String.wordCount: Int get() = split("\\s+".toRegex()).size println("Hello world from Kotlin".wordCount) // 4 // Extension property with backing field not allowed // Must use get() or set() var StringBuilder.lastChar: Char get() = this[length - 1] set(value) { this[length - 1] = value }
4.3 Generic Extensions
// Generic extension for any type fun <T> T.print(): T { println(this) return this } "Hello".print().uppercase().print() // Extension with type constraints fun <T : Comparable<T>> List<T>.second(): T? = if (size >= 2) this[1] else null // Extension for collections fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null println(listOf(1, 2, 3).secondOrNull()) // 2
4.4 Extensions on Companion Objects
class User(val name: String) { companion object { // Regular companion object function } } // Extension on companion object fun User.Companion.fromJson(json: String): User { // Parse JSON and create User return User("Parsed from JSON") } // Usage val user = User.fromJson("""{"name": "Alice"}""")
4.5 Scope-Limited Extensions
// Extension only visible in this scope class HtmlBuilder { private fun String.escapeHtml(): String = replace("&", "&") .replace("<", "<") .replace(">", ">") fun paragraph(text: String): String = "<p>${text.escapeHtml()}</p>" } // Extension not visible outside HtmlBuilder
4.6 Common Extension Patterns
// Collection extensions fun <T> List<T>.splitAt(index: Int): Pair<List<T>, List<T>> = take(index) to drop(index) fun <K, V> Map<K, V>.getOrThrow(key: K): V = this[key] ?: throw NoSuchElementException("Key $key not found") // Validation extensions fun String.requireNotBlank(fieldName: String): String { require(isNotBlank()) { "$fieldName cannot be blank" } return this } // Transformation extensions fun String.toTitleCase(): String = split(" ").joinToString(" ") { word -> word.lowercase().replaceFirstChar { it.uppercase() } } // Utility extensions inline fun <T> T.applyIf(condition: Boolean, block: T.() -> Unit): T { if (condition) block() return this } val list = mutableListOf(1, 2, 3) .applyIf(someCondition) { add(4) }
6. Scope Functions
Scope functions execute a block of code within the context of an object.
5.1 let - Null Safety and Transformations
// Execute block if not null val name: String? = "Alice" name?.let { println("Name is $it") println("Length is ${it.length}") } // Transform and return result val length = name?.let { it.length } ?: 0 // Chain operations val result = getUserInput() ?.trim() ?.let { parseUserData(it) } ?.let { validateData(it) } ?.let { saveToDatabase(it) } // Scope to new variable val validatedEmail = email?.let { value -> if (value.contains("@")) value else null }
5.2 run - Object Configuration and Computation
// Run on nullable receiver val config: Config? = loadConfig() val port = config?.run { validate() applyDefaults() port } ?: 8080 // Compute with context val hexColor = run { val r = (color shr 16) and 0xFF val g = (color shr 8) and 0xFF val b = color and 0xFF "#%02X%02X%02X".format(r, g, b) } // Scope local variables val result = run { val a = computeA() val b = computeB() a + b }
5.3 with - Multiple Operations on Same Object
// Non-extension function val person = Person() with(person) { name = "Alice" age = 30 email = "alice@example.com" validate() } // Return value from with val result = with(StringBuilder()) { append("Hello") append(" ") append("World") toString() } // Multiple property access val info = with(user) { "Name: $name, Email: $email, Active: $isActive" }
5.4 apply - Object Configuration (Returns Receiver)
// Builder-style configuration val person = Person().apply { name = "Alice" age = 30 email = "alice@example.com" } // Chain multiple apply blocks val request = HttpRequest().apply { url = "https://api.example.com" method = "POST" }.apply { addHeader("Content-Type", "application/json") addHeader("Authorization", "Bearer $token") }.apply { body = """{"data": "value"}""" } // Conditional configuration val user = User().apply { name = "Alice" if (isAdmin) { role = "ADMIN" permissions = adminPermissions } }
5.5 also - Side Effects (Returns Receiver)
// Logging and debugging val result = processData(input) .also { println("Processed result: $it") } .also { log.debug("Result details: $it") } // Validation before return fun createUser(name: String): User { return User(name).also { require(it.isValid()) { "Invalid user" } saveToDatabase(it) } } // Multiple side effects val file = File("output.txt") .also { it.createNewFile() } .also { it.setWritable(true) } .also { it.writeText("Hello") }
5.6 Choosing the Right Scope Function
// Decision guide: // let - Transform nullable, introduce local scope val length: Int = nullableString?.let { it.length } ?: 0 // run - Compute value using multiple statements val total = run { val subtotal = items.sumOf { it.price } val tax = subtotal * 0.1 subtotal + tax } // with - Multiple operations, focus on context with(canvas) { drawLine(0, 0, width, height) drawCircle(width / 2, height / 2, 50) } // apply - Configure object, return it val person = Person().apply { name = "Alice" age = 30 } // also - Side effects, return object val saved = user.also { repository.save(it) }
5.7 Nested Scope Functions
// Chaining different scope functions val result = fetchUser(id) ?.let { user -> user.apply { lastLoginAt = Instant.now() } } ?.also { repository.save(it) } ?.let { user -> UserDto(user.id, user.name) } // Complex transformations val config = loadConfig() ?.run { validate() this } ?.apply { applyDefaults() } ?.also { logger.info("Config loaded: $it") } ?: DefaultConfig()
7. Collections and Sequences
Kotlin provides rich collection APIs with eager (collections) and lazy (sequences) evaluation.
6.1 Collection Basics
// List (immutable) val numbers = listOf(1, 2, 3, 4, 5) val empty = emptyList<Int>() // MutableList val mutable = mutableListOf(1, 2, 3) mutable.add(4) mutable.removeAt(0) // Set (unique elements) val uniqueNumbers = setOf(1, 2, 2, 3, 3) // [1, 2, 3] val mutableSet = mutableSetOf<String>() // Map val ages = mapOf("Alice" to 30, "Bob" to 25) val mutableMap = mutableMapOf<String, Int>() mutableMap["Charlie"] = 35
6.2 Collection Transformations
val numbers = listOf(1, 2, 3, 4, 5) // map - Transform each element val squared = numbers.map { it * it } // [1, 4, 9, 16, 25] // filter - Keep matching elements val evens = numbers.filter { it % 2 == 0 } // [2, 4] // mapNotNull - Transform and filter nulls val strings = listOf("1", "2", "abc", "3") val parsed = strings.mapNotNull { it.toIntOrNull() } // [1, 2, 3] // flatMap - Transform and flatten val nested = listOf(listOf(1, 2), listOf(3, 4)) val flattened = nested.flatMap { it } // [1, 2, 3, 4] // partition - Split into two lists val (evens, odds) = numbers.partition { it % 2 == 0 } // groupBy - Group into map val byParity = numbers.groupBy { if (it % 2 == 0) "even" else "odd" } // {odd=[1, 3, 5], even=[2, 4]}
6.3 Collection Aggregations
val numbers = listOf(1, 2, 3, 4, 5) // sum, average, min, max val sum = numbers.sum() // 15 val avg = numbers.average() // 3.0 val max = numbers.maxOrNull() // 5 // sumOf - Transform then sum val lengthSum = listOf("a", "bb", "ccc").sumOf { it.length } // 6 // reduce - Combine elements val product = numbers.reduce { acc, n -> acc * n } // 120 // fold - Reduce with initial value val sumPlus10 = numbers.fold(10) { acc, n -> acc + n } // 25 // joinToString - Create string val csv = numbers.joinToString(separator = ",") // "1,2,3,4,5" val custom = numbers.joinToString( separator = " | ", prefix = "[", postfix = "]", limit = 3, truncated = "..." ) { "Item $it" } // [Item 1 | Item 2 | Item 3 | ...]
6.4 Collection Queries
val numbers = listOf(1, 2, 3, 4, 5) // any, all, none val hasEven = numbers.any { it % 2 == 0 } // true val allPositive = numbers.all { it > 0 } // true val noneNegative = numbers.none { it < 0 } // true // find, first, last val firstEven = numbers.find { it % 2 == 0 } // 2 val firstOrNull = numbers.firstOrNull { it > 10 } // null val last = numbers.last() // 5 // take, drop, slice val first3 = numbers.take(3) // [1, 2, 3] val without2 = numbers.drop(2) // [3, 4, 5] val middle = numbers.slice(1..3) // [2, 3, 4] // distinct, distinctBy val duplicates = listOf(1, 2, 2, 3, 3, 3) val unique = duplicates.distinct() // [1, 2, 3] data class Person(val name: String, val age: Int) val people = listOf(Person("Alice", 30), Person("Bob", 30)) val byAge = people.distinctBy { it.age } // [Person("Alice", 30)]
6.5 Sequences (Lazy Evaluation)
// Sequence - lazy evaluation val sequence = sequenceOf(1, 2, 3, 4, 5) // Convert to sequence for lazy evaluation val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) // Eager (collection) - all operations execute immediately val eagerResult = numbers .map { println("map $it"); it * 2 } // Prints 10 times .filter { println("filter $it"); it > 10 } // Prints 10 times .take(2) // Still processed all elements // Lazy (sequence) - operations execute on demand val lazyResult = numbers.asSequence() .map { println("map $it"); it * 2 } .filter { println("filter $it"); it > 10 } .take(2) // Only processes until 2 elements found .toList() // Generate infinite sequence val naturalNumbers = generateSequence(1) { it + 1 } val first100Even = naturalNumbers .filter { it % 2 == 0 } .take(100) .toList() // Sequence from function val fibonacci = sequence { var a = 0 var b = 1 while (true) { yield(a) val next = a + b a = b b = next } } val first10Fib = fibonacci.take(10).toList()
6.6 Collection Building
// buildList - Builder function val squares = buildList { for (i in 1..5) { add(i * i) } if (someCondition) { add(100) } } // buildSet val uniqueWords = buildSet { add("hello") add("world") add("hello") // Duplicate ignored } // buildMap val characterCounts = buildMap { for (char in "hello") { put(char, getOrDefault(char, 0) + 1) } } // associate - Build map from collection val users = listOf(User(1, "Alice"), User(2, "Bob")) val byId = users.associateBy { it.id } val byName = users.associateBy({ it.name }, { it.id })
6.7 Advanced Collection Patterns
// Windowed - Sliding windows val numbers = listOf(1, 2, 3, 4, 5) val windows = numbers.windowed(size = 3, step = 1) // [[1, 2, 3], [2, 3, 4], [3, 4, 5]] // Chunked - Fixed-size groups val chunks = numbers.chunked(2) // [[1, 2], [3, 4], [5]] // Zip - Combine two collections val names = listOf("Alice", "Bob") val ages = listOf(30, 25) val pairs = names.zip(ages) // [(Alice, 30), (Bob, 25)] val combined = names.zip(ages) { name, age -> "$name is $age" } // Union, intersect, subtract (Sets) val set1 = setOf(1, 2, 3) val set2 = setOf(2, 3, 4) val union = set1 union set2 // [1, 2, 3, 4] val intersect = set1 intersect set2 // [2, 3] val subtract = set1 subtract set2 // [1]
8. Higher-Order Functions and Lambdas
Functions that take functions as parameters or return functions.
7.1 Lambda Syntax
// Lambda expression val sum: (Int, Int) -> Int = { a, b -> a + b } println(sum(2, 3)) // 5 // Single parameter - implicit 'it' val square: (Int) -> Int = { it * it } println(square(5)) // 25 // No parameters val greeting: () -> String = { "Hello" } println(greeting()) // Hello // Multiple statements val process: (Int) -> Int = { value -> val doubled = value * 2 val squared = doubled * doubled squared // Last expression is return value }
7.2 Function Types
// Function type notation val operation: (Int, Int) -> Int = { a, b -> a + b } // Nullable function type val nullableOp: ((Int, Int) -> Int)? = null // Function type with receiver val stringBuilder: StringBuilder.() -> Unit = { append("Hello") append(" World") } // Higher-order function fun calculate(a: Int, b: Int, op: (Int, Int) -> Int): Int { return op(a, b) } println(calculate(5, 3, { a, b -> a + b })) // 8 println(calculate(5, 3) { a, b -> a * b }) // 15 (trailing lambda)
7.3 Trailing Lambda Syntax
// When last parameter is function, move outside parentheses val numbers = listOf(1, 2, 3, 4, 5) // Standard syntax numbers.filter({ it % 2 == 0 }) // Trailing lambda numbers.filter { it % 2 == 0 } // Only lambda parameter - omit parentheses numbers.forEach { println(it) } // Multiple parameters with trailing lambda numbers.fold(0) { acc, n -> acc + n }
7.4 Function References
// Top-level function reference fun isEven(n: Int): Boolean = n % 2 == 0 val numbers = listOf(1, 2, 3, 4, 5) val evens = numbers.filter(::isEven) // Member function reference class StringUtils { fun isPalindrome(s: String): Boolean { return s == s.reversed() } } val utils = StringUtils() val words = listOf("level", "hello", "radar") val palindromes = words.filter(utils::isPalindrome) // Constructor reference data class Person(val name: String, val age: Int) val names = listOf("Alice", "Bob") val people = names.map(::Person) // Partial application not supported
7.5 Inline Functions
// Inline function - lambda code inlined at call site inline fun <T> measureTime(block: () -> T): T { val start = System.currentTimeMillis() val result = block() val end = System.currentTimeMillis() println("Took ${end - start}ms") return result } // Usage - no lambda object created val result = measureTime { Thread.sleep(100) 42 } // noinline - Don't inline specific lambda inline fun complexOperation( inline operation: () -> Unit, noinline callback: () -> Unit ) { operation() storeCallback(callback) // Can store noinline lambda } // crossinline - Lambda can't use non-local returns inline fun runAsync(crossinline block: () -> Unit) { thread { block() // Would fail without crossinline if block has return } }
7.6 Returning Functions
// Function that returns function fun makeMultiplier(factor: Int): (Int) -> Int { return { value -> value * factor } } val double = makeMultiplier(2) val triple = makeMultiplier(3) println(double(5)) // 10 println(triple(5)) // 15 // Closure captures variables fun makeCounter(): () -> Int { var count = 0 return { ++count } } val counter = makeCounter() println(counter()) // 1 println(counter()) // 2 println(counter()) // 3
7.7 Common Higher-Order Function Patterns
// Retry pattern inline fun <T> retry( times: Int = 3, delay: Long = 1000, block: () -> T ): T { repeat(times - 1) { try { return block() } catch (e: Exception) { Thread.sleep(delay) } } return block() // Last attempt throws if fails } // Resource management inline fun <T : AutoCloseable, R> T.use(block: (T) -> R): R { try { return block(this) } finally { close() } } // Memoization fun <P, R> memoize(fn: (P) -> R): (P) -> R { val cache = mutableMapOf<P, R>() return { param -> cache.getOrPut(param) { fn(param) } } } val fibonacci = memoize<Int, Long> { n -> if (n <= 1) n.toLong() else fibonacci(n - 1) + fibonacci(n - 2) }
9. Coroutines Basics
Kotlin coroutines provide asynchronous programming support with sequential-looking code.
8.1 Suspend Functions
import kotlinx.coroutines.* // Suspend function - can only be called from coroutine or another suspend function suspend fun fetchUser(id: Long): User { delay(1000) // Non-blocking delay return User(id, "Alice") } suspend fun fetchPosts(userId: Long): List<Post> { delay(500) return listOf(Post(1, "Hello"), Post(2, "World")) } // Calling suspend functions suspend fun loadUserData(id: Long): UserData { val user = fetchUser(id) // Sequential execution val posts = fetchPosts(user.id) return UserData(user, posts) }
8.2 Coroutine Builders
import kotlinx.coroutines.* // launch - Fire and forget fun main() = runBlocking { launch { delay(1000) println("World") } println("Hello") // Output: Hello, World (after 1 second) } // async - Return value fun main() = runBlocking { val deferred = async { delay(1000) "Result" } println("Waiting...") val result = deferred.await() // Suspend until result ready println(result) } // runBlocking - Bridge to coroutine world fun main() { runBlocking { launch { delay(1000) println("Coroutine") } println("Main") } println("Done") }
8.3 Concurrent Execution
suspend fun loadUserData(id: Long): UserData = coroutineScope { // Sequential (slow) val user = fetchUser(id) val posts = fetchPosts(id) // Concurrent (fast) val userDeferred = async { fetchUser(id) } val postsDeferred = async { fetchPosts(id) } UserData( user = userDeferred.await(), posts = postsDeferred.await() ) } // Multiple concurrent operations suspend fun loadDashboard(): Dashboard = coroutineScope { val user = async { fetchUser() } val notifications = async { fetchNotifications() } val messages = async { fetchMessages() } val stats = async { fetchStats() } Dashboard( user.await(), notifications.await(), messages.await(), stats.await() ) }
8.4 Coroutine Context and Dispatchers
import kotlinx.coroutines.* // Dispatchers.Default - CPU-intensive work launch(Dispatchers.Default) { val result = performHeavyComputation() } // Dispatchers.IO - I/O operations launch(Dispatchers.IO) { val data = readFromDatabase() } // Dispatchers.Main - UI operations (Android/Desktop) launch(Dispatchers.Main) { updateUI(data) } // Switch contexts suspend fun fetchAndDisplay() { val data = withContext(Dispatchers.IO) { fetchFromNetwork() } withContext(Dispatchers.Main) { displayData(data) } }
8.5 Structured Concurrency
// coroutineScope - Creates scope, suspends until all children complete suspend fun processAll(items: List<Item>): List<Result> = coroutineScope { items.map { item -> async { processItem(item) } }.awaitAll() // Waits for all to complete } // supervisorScope - Children failures don't cancel siblings suspend fun fetchMultiple(): List<Data?> = supervisorScope { val results = listOf( async { fetchFromSource1() }, async { fetchFromSource2() }, // If this fails... async { fetchFromSource3() } // ...this still runs ) results.map { try { it.await() } catch (e: Exception) { null } } } // Cancellation val job = launch { repeat(1000) { i -> println("Job: $i") delay(500) } } delay(2000) job.cancel() // Cancel the job job.join() // Wait for cancellation to complete
8.6 Exception Handling in Coroutines
// try-catch in coroutines suspend fun safeOperation(): Result<Data> { return try { val data = fetchData() Result.Success(data) } catch (e: Exception) { Result.Error(e) } } // CoroutineExceptionHandler val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") } val scope = CoroutineScope(Dispatchers.Default + handler) scope.launch { throw Exception("Failed") } // supervisorScope with exception handling suspend fun robustFetch(): List<Data?> = supervisorScope { val sources = listOf(source1, source2, source3) sources.map { source -> async { try { source.fetch() } catch (e: Exception) { logger.error("Failed to fetch from $source", e) null } } }.awaitAll() }
8.7 Common Coroutine Patterns
// Timeout suspend fun fetchWithTimeout(id: Long): User? { return try { withTimeout(5000) { // 5 second timeout fetchUser(id) } } catch (e: TimeoutCancellationException) { null } } // Retry with delay suspend fun fetchWithRetry(maxAttempts: Int = 3): Data { repeat(maxAttempts - 1) { attempt -> try { return fetchData() } catch (e: Exception) { delay(1000 * (attempt + 1)) // Exponential backoff } } return fetchData() // Last attempt } // Parallel processing with limit suspend fun processInParallel( items: List<Item>, concurrency: Int = 10 ): List<Result> = coroutineScope { items.chunked(concurrency).flatMap { chunk -> chunk.map { item -> async { processItem(item) } }.awaitAll() } }
10. Kotlin Idioms
Idiomatic Kotlin patterns that make code more concise and expressive.
9.1 String Templates
val name = "Alice" val age = 30 // Simple interpolation val greeting = "Hello, $name!" // Expressions in templates val info = "Name: $name, Age: $age, Adult: ${age >= 18}" // Multi-line strings val json = """ { "name": "$name", "age": $age, "address": { "city": "New York" } } """.trimIndent() // Raw strings with custom margin val sql = """ |SELECT * |FROM users |WHERE name = '$name' |AND age > $age """.trimMargin()
9.2 Destructuring
// Data classes data class User(val id: Long, val name: String, val email: String) val user = User(1, "Alice", "alice@example.com") val (id, name, email) = user // Pairs and Triples val pair = "key" to "value" val (key, value) = pair // In loops val map = mapOf("a" to 1, "b" to 2, "c" to 3) for ((key, value) in map) { println("$key -> $value") } // Component functions for custom types class Range(val start: Int, val end: Int) { operator fun component1() = start operator fun component2() = end } val range = Range(1, 10) val (start, end) = range
9.3 Smart Casts
// After type check, automatic cast fun process(value: Any) { if (value is String) { // value is automatically String here println(value.length) } } // Smart cast with when fun describe(obj: Any): String = when (obj) { is String -> "String of length ${obj.length}" is Int -> "Integer: $obj" is List<*> -> "List of size ${obj.size}" else -> "Unknown type" } // Smart cast after null check fun printLength(str: String?) { if (str != null) { // str is automatically String (non-null) println(str.length) } } // Smart cast with require/check fun process(value: Any) { require(value is String) { "Must be String" } // value is String here println(value.uppercase()) }
9.4 Named Arguments and Default Parameters
// Default parameters fun createUser( name: String, age: Int = 18, email: String? = null, isActive: Boolean = true ) = User(name, age, email, isActive) // Call with defaults val user1 = createUser("Alice") val user2 = createUser("Bob", age = 25) val user3 = createUser("Charlie", email = "charlie@example.com", age = 30) // Named arguments improve readability val rect = Rectangle( width = 100.0, height = 50.0, color = Color.RED, borderWidth = 2.0 ) // Reordering with named arguments fun formatDate( day: Int, month: Int, year: Int, separator: String = "-" ): String = "$year$separator$month$separator$day" val date = formatDate(year = 2024, month = 3, day = 15)
9.5 Single Expression Functions
// Function with expression body fun square(x: Int): Int = x * x // Type inference fun double(x: Int) = x * 2 // With when expression fun sign(x: Int) = when { x > 0 -> "positive" x < 0 -> "negative" else -> "zero" } // With if expression fun max(a: Int, b: Int) = if (a > b) a else b // Chained calls fun processName(name: String) = name .trim() .lowercase() .replaceFirstChar { it.uppercase() }
9.6 Operator Overloading
// Arithmetic operators data class Point(val x: Int, val y: Int) { operator fun plus(other: Point) = Point(x + other.x, y + other.y) operator fun minus(other: Point) = Point(x - other.x, y - other.y) operator fun times(scale: Int) = Point(x * scale, y * scale) } val p1 = Point(1, 2) val p2 = Point(3, 4) val p3 = p1 + p2 // Point(4, 6) val p4 = p1 * 2 // Point(2, 4) // Comparison operators data class Version(val major: Int, val minor: Int, val patch: Int) : Comparable<Version> { override fun compareTo(other: Version): Int { if (major != other.major) return major - other.major if (minor != other.minor) return minor - other.minor return patch - other.patch } } val v1 = Version(1, 2, 3) val v2 = Version(1, 3, 0) println(v1 < v2) // true // Index operators class MutableGrid<T>(private val data: Array<Array<T>>) { operator fun get(row: Int, col: Int): T = data[row][col] operator fun set(row: Int, col: Int, value: T) { data[row][col] = value } } // Invoke operator class Greeter(val greeting: String) { operator fun invoke(name: String) = "$greeting, $name!" } val greet = Greeter("Hello") println(greet("Alice")) // Hello, Alice!
9.7 Delegation
// Property delegation class User { // Lazy initialization val expensiveData: Data by lazy { loadExpensiveData() } // Observable property var name: String by Delegates.observable("<no name>") { _, old, new -> println("Name changed from $old to $new") } // Veto changes var age: Int by Delegates.vetoable(0) { _, old, new -> new >= 0 // Only allow non-negative ages } } // Map delegation class Config(map: Map<String, Any?>) { val host: String by map val port: Int by map val timeout: Long by map } val config = Config(mapOf( "host" to "localhost", "port" to 8080, "timeout" to 5000L )) // Class delegation interface Repository { fun save(data: Data) fun load(id: Long): Data } class CachingRepository( private val delegate: Repository, private val cache: Cache ) : Repository by delegate { // Override specific methods override fun load(id: Long): Data { return cache.get(id) ?: delegate.load(id).also { cache.put(id, it) } } }
9.8 Type Aliases
// Type alias for complex types typealias UserId = Long typealias Predicate<T> = (T) -> Boolean typealias StringMap = Map<String, String> // Generic type alias typealias ResultCallback<T> = (Result<T>) -> Unit // Usage fun findUser(id: UserId): User? = database.find(id) fun filter(items: List<Int>, predicate: Predicate<Int>): List<Int> = items.filter(predicate) val config: StringMap = mapOf("key" to "value") // Type alias for function types typealias ClickHandler = (View, Int) -> Unit fun setClickListener(handler: ClickHandler) { // ... }
11. Java Interoperability
Kotlin provides seamless interop with Java code.
10.1 Calling Java from Kotlin
// Java getter/setter accessed as properties val list = ArrayList<String>() // Java class list.add("Kotlin") // Java method println(list.size) // Java getSize() -> size property // Java void methods val file = File("test.txt") file.createNewFile() // Java void method // Static methods val sqrt = Math.sqrt(16.0) // Java static method // Object methods from Java val stream = File("data.txt").inputStream()
10.2 Null Safety with Java
// Platform types (Type!) - nullable unknown val javaString: String = getJavaString() // May throw NPE if null // Explicit nullable handling val safeString: String? = getJavaString() val length: Int = safeString?.length ?: 0 // Annotated Java code // If Java uses @Nullable/@NotNull val nullable: String? = getAnnotatedNullable() // Kotlin knows it's nullable val nonNull: String = getAnnotatedNonNull() // Kotlin knows it's not null
10.3 Calling Kotlin from Java
// Kotlin code class KotlinClass { // Becomes static method in Java companion object { @JvmStatic fun create(): KotlinClass = KotlinClass() } // Default parameters need @JvmOverloads @JvmOverloads fun greet(name: String, greeting: String = "Hello") = "$greeting, $name" } // Java usage KotlinClass obj = KotlinClass.create(); String msg1 = obj.greet("Alice"); String msg2 = obj.greet("Bob", "Hi");
10.4 JVM Annotations
// @JvmName - Change JVM name @file:JvmName("Utils") package com.example fun helper() { } // Java: Utils.helper() // @JvmStatic - Static method/field object Singleton { @JvmStatic fun getInstance(): Singleton = this @JvmField val CONSTANT = 42 } // @JvmOverloads - Generate overloads for default params class User @JvmOverloads constructor( val name: String, val age: Int = 0, val email: String? = null ) // Java can call: new User("Alice"), new User("Alice", 30), etc. // @Throws - Declare checked exceptions @Throws(IOException::class) fun readFile(path: String): String { return File(path).readText() }
10.5 SAM Conversions
// Java interface with single abstract method // interface Runnable { void run(); } // Kotlin lambda automatically converts val runnable = Runnable { println("Running") } // Java functional interfaces val listener = ActionListener { event -> println("Action performed: $event") } // Explicit SAM constructor val callback = Callback { result -> handleResult(result) }
10.6 Handling Java Collections
// Java collections are mutable in Kotlin val javaList: MutableList<String> = ArrayList() javaList.add("Kotlin") // Converting to Kotlin immutable val kotlinList: List<String> = javaList.toList() // Extension functions work on Java collections val upperCased = javaList.map { it.uppercase() } // Kotlin collection to Java val kotlinSet = setOf(1, 2, 3) val javaSet: java.util.Set<Int> = kotlinSet.toSet()
12. Best Practices
11.1 Prefer Immutability
// Use val instead of var val name = "Alice" // Preferred var mutableName = "Bob" // Use when mutation needed // Immutable collections val list = listOf(1, 2, 3) // Preferred val mutableList = mutableListOf(1, 2, 3) // Use when mutation needed // Immutable data classes data class User(val id: Long, val name: String) // Use copy() for changes val user = User(1, "Alice") val updated = user.copy(name = "Alicia")
11.2 Null Safety Best Practices
// Prefer non-null types fun greet(name: String) { // Preferred println("Hello, $name") } // Only use nullable when truly optional fun findUser(id: Long): User? // Returns null if not found // Avoid !! operator val user = getUser()!! // Avoid - may throw NPE // Better alternatives val user = getUser() ?: return // Early return val user = getUser() ?: throw IllegalStateException("User required") val name = getUser()?.name ?: "Unknown"
11.3 Scope Function Usage
// Use let for null safety and transformations user?.let { u -> processUser(u) saveUser(u) } // Use apply for object configuration val person = Person().apply { name = "Alice" age = 30 } // Use also for side effects val user = createUser().also { logger.info("Created user: ${it.id}") cache.put(it.id, it) } // Use run for complex computations val result = run { val a = computeA() val b = computeB() combine(a, b) } // Use with for multiple operations on same object with(canvas) { drawLine(0, 0, 100, 100) drawCircle(50, 50, 25) }
11.4 Prefer Expressions Over Statements
// Use when expression val result = when (status) { Status.SUCCESS -> handleSuccess() Status.ERROR -> handleError() Status.PENDING -> handlePending() } // Use if expression val max = if (a > b) a else b // Use try expression val result = try { dangerousOperation() } catch (e: Exception) { defaultValue }
11.5 Leverage Type Inference
// Let compiler infer types when obvious val name = "Alice" // Type inferred as String val numbers = listOf(1, 2, 3) // List<Int> // Specify type when it aids readability val result: Result<User> = processUser() // Clarity val callback: (Int) -> Unit = { processValue(it) } // Clarity
11.6 Use Extension Functions Wisely
// Good: Utility operations fun String.isEmail(): Boolean = contains("@") && contains(".") // Good: Domain-specific operations fun User.isAdult(): Boolean = age >= 18 // Avoid: Extending too broadly fun Any.doSomething() { } // Too broad, avoid // Avoid: Complex logic that should be in a class // Long extension functions with many dependencies -> use class instead
13. Common Patterns
12.1 Builder Pattern
// Kotlin builder with DSL class HttpRequest private constructor( val url: String, val method: String, val headers: Map<String, String>, val body: String? ) { class Builder { var url: String = "" var method: String = "GET" private val headers = mutableMapOf<String, String>() var body: String? = null fun header(key: String, value: String) = apply { headers[key] = value } fun build() = HttpRequest(url, method, headers, body) } } // Usage with apply val request = HttpRequest.Builder().apply { url = "https://api.example.com" method = "POST" header("Content-Type", "application/json") body = """{"key": "value"}""" }.build() // Type-safe builder DSL class Html { private val children = mutableListOf<Tag>() fun body(init: Body.() -> Unit) { children.add(Body().apply(init)) } } class Body { fun p(text: String) { // Add paragraph } } // Usage fun html(init: Html.() -> Unit): Html = Html().apply(init) val page = html { body { p("Hello") p("World") } }
12.2 Sealed Class for State
sealed class UiState<out T> { data object Idle : UiState<Nothing>() data object Loading : UiState<Nothing>() data class Success<T>(val data: T) : UiState<T>() data class Error(val exception: Exception) : UiState<Nothing>() } // State machine class ViewModel { var state: UiState<User> = UiState.Idle private set suspend fun loadUser(id: Long) { state = UiState.Loading state = try { val user = repository.fetchUser(id) UiState.Success(user) } catch (e: Exception) { UiState.Error(e) } } } // Rendering based on state fun render(state: UiState<User>) { when (state) { UiState.Idle -> showEmpty() UiState.Loading -> showSpinner() is UiState.Success -> showUser(state.data) is UiState.Error -> showError(state.exception) } }
12.3 Repository Pattern
// Repository interface interface UserRepository { suspend fun getUser(id: Long): User? suspend fun saveUser(user: User) suspend fun deleteUser(id: Long) } // Implementation with caching class CachedUserRepository( private val api: UserApi, private val cache: Cache ) : UserRepository { override suspend fun getUser(id: Long): User? { return cache.get(id) ?: api.fetchUser(id)?.also { cache.put(id, it) } } override suspend fun saveUser(user: User) { api.saveUser(user) cache.put(user.id, user) } override suspend fun deleteUser(id: Long) { api.deleteUser(id) cache.remove(id) } }
12.4 Validation with Result
sealed class ValidationResult<out T> { data class Valid<T>(val value: T) : ValidationResult<T>() data class Invalid(val errors: List<String>) : ValidationResult<Nothing>() } fun validateUser( name: String, email: String, age: Int ): ValidationResult<User> { val errors = mutableListOf<String>() if (name.isBlank()) errors.add("Name cannot be blank") if (!email.contains("@")) errors.add("Invalid email") if (age < 0) errors.add("Age must be positive") return if (errors.isEmpty()) { ValidationResult.Valid(User(name, email, age)) } else { ValidationResult.Invalid(errors) } } // Usage when (val result = validateUser(name, email, age)) { is ValidationResult.Valid -> saveUser(result.value) is ValidationResult.Invalid -> showErrors(result.errors) }
14. Troubleshooting
13.1 NullPointerException
// Problem: Calling !! on null val name = getName()!! // NPE if getName() returns null // Solution: Use safe operators val name = getName() ?: "default" val length = getName()?.length ?: 0 // Solution: Early return val name = getName() ?: return processName(name)
13.2 Type Mismatch
// Problem: Platform type confusion val javaString: String = getJavaString() // May be null // Solution: Use nullable type val javaString: String? = getJavaString() // Problem: Collection variance val list: List<Any> = listOf<String>("a") // OK - covariant val mutableList: MutableList<Any> = mutableListOf<String>("a") // Error - invariant // Solution: Use read-only List or explicit type val list: List<Any> = mutableListOf<String>("a") // OK
13.3 Coroutine Cancellation
// Problem: Ignoring cancellation suspend fun process() { while (true) { // Expensive operation - can't cancel processItem() } } // Solution: Check isActive or use cancellable functions suspend fun process() = coroutineScope { while (isActive) { processItem() } } // Or use delay which is cancellable suspend fun process() { while (true) { processItem() delay(100) // Cancellation point } }
13.4 Memory Leaks with Coroutines
// Problem: GlobalScope launches never cleaned GlobalScope.launch { // This lives forever } // Solution: Use proper scope class ViewModel : CoroutineScope { private val job = Job() override val coroutineContext = Dispatchers.Main + job fun loadData() { launch { // Tied to ViewModel lifecycle // ... } } fun onCleared() { job.cancel() // Clean up when done } }
13.5 Lazy Initialization Issues
// Problem: Lazy not thread-safe by default (it is by default, but showing pattern) val data by lazy { expensiveComputation() } // For truly thread-unsafe scenarios val data by lazy(LazyThreadSafetyMode.NONE) { expensiveComputation() // Only if guaranteed single-threaded } // Problem: Lazy val in data class data class User(val name: String) { val computed by lazy { expensiveOp() } // Not in equals/hashCode } // Solution: Make it explicit data class User(val name: String) { fun getComputed() = computedValue private val computedValue by lazy { expensiveOp() } }
15. Testing
Kotlin's testing ecosystem provides powerful frameworks with idiomatic DSL support, extension functions, and coroutine testing utilities.
13.1 JUnit 5 with Kotlin
import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* class CalculatorTest { private lateinit var calculator: Calculator @BeforeEach fun setUp() { calculator = Calculator() } @Test fun `should add two numbers`() { val result = calculator.add(2, 3) assertEquals(5, result) } @Test fun `should throw on division by zero`() { assertThrows<ArithmeticException> { calculator.divide(10, 0) } } @Test @DisplayName("Addition is commutative") fun additionIsCommutative() { assertEquals( calculator.add(2, 3), calculator.add(3, 2) ) } // Parameterized tests @ParameterizedTest @ValueSource(ints = [1, 2, 3, 4, 5]) fun `should be positive`(number: Int) { assertTrue(number > 0) } @ParameterizedTest @CsvSource( "1, 2, 3", "10, 20, 30", "-1, 1, 0" ) fun `should add correctly`(a: Int, b: Int, expected: Int) { assertEquals(expected, calculator.add(a, b)) } }
13.2 Kotest Framework
import io.kotest.core.spec.style.* import io.kotest.matchers.* import io.kotest.matchers.collections.* import io.kotest.matchers.string.* // BehaviorSpec - BDD style class UserServiceSpec : BehaviorSpec({ Given("a new user") { val user = User("Alice", "alice@example.com") When("we save the user") { val saved = userService.save(user) Then("the user should have an ID") { saved.id shouldNotBe null } Then("the user should be retrievable") { val found = userService.findById(saved.id!!) found shouldBe saved } } } }) // FunSpec - simple function-based tests class CalculatorSpec : FunSpec({ test("addition should work") { val result = Calculator().add(2, 3) result shouldBe 5 } test("division by zero should throw") { shouldThrow<ArithmeticException> { Calculator().divide(10, 0) } } context("with positive numbers") { test("multiplication should be positive") { Calculator().multiply(2, 3) shouldBe 6 } } }) // StringSpec - minimal style class StringUtilsSpec : StringSpec({ "length should return string length" { "hello".length shouldBe 5 } "startsWith should check prefix" { "hello" should startWith("hel") } "uppercase should convert to upper" { "hello".uppercase() shouldBe "HELLO" } }) // DescribeSpec - Jasmine/Mocha style class ListSpec : DescribeSpec({ describe("a list") { val list = listOf(1, 2, 3) it("should have size 3") { list.size shouldBe 3 } it("should contain 2") { list shouldContain 2 } describe("when filtered") { val filtered = list.filter { it > 1 } it("should have size 2") { filtered.size shouldBe 2 } it("should only contain values > 1") { filtered shouldContainExactly listOf(2, 3) } } } })
13.3 Kotest Matchers
import io.kotest.matchers.* import io.kotest.matchers.collections.* import io.kotest.matchers.string.* import io.kotest.matchers.types.* // Equality and comparison value shouldBe expected value shouldNotBe unexpected value shouldBeGreaterThan 5 value shouldBeLessThanOrEqual 10 // Null checks value.shouldNotBeNull() value.shouldBeNull() // Type checks value shouldBeInstanceOf<String>() value.shouldBeTypeOf<User>() // String matchers str shouldStartWith "prefix" str shouldEndWith "suffix" str shouldContain "middle" str shouldMatch "\\d+".toRegex() str.shouldBeEmpty() str.shouldBeBlank() // Collection matchers list shouldContain element list shouldContainAll listOf(1, 2, 3) list shouldContainExactly listOf(1, 2, 3) list shouldHaveSize 5 list.shouldBeEmpty() list.shouldBeSorted() // Map matchers map shouldContainKey "key" map shouldContainValue 42 map shouldContainAll mapOf("a" to 1, "b" to 2) // Exception matchers shouldThrow<IllegalArgumentException> { validate(null) } val exception = shouldThrow<CustomException> { dangerousOperation() } exception.message shouldBe "Expected error" // Boolean matchers condition.shouldBeTrue() condition.shouldBeFalse() // Custom matchers data class User(val name: String, val age: Int) fun beAdult() = object : Matcher<User> { override fun test(value: User) = MatcherResult( value.age >= 18, { "User ${value.name} should be adult but was ${value.age}" }, { "User ${value.name} should not be adult" } ) } user should beAdult()
13.4 MockK - Mocking Library
import io.mockk.* import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.junit5.MockKExtension @ExtendWith(MockKExtension::class) class UserServiceTest { @MockK private lateinit var repository: UserRepository @InjectMockKs private lateinit var service: UserService @Test fun `should find user by id`() { // Given val user = User(1, "Alice") every { repository.findById(1) } returns user // When val result = service.getUser(1) // Then result shouldBe user verify { repository.findById(1) } } @Test fun `should throw when user not found`() { // Given every { repository.findById(any()) } returns null // When/Then shouldThrow<UserNotFoundException> { service.getUser(999) } } @Test fun `should save user`() { // Given val user = User(0, "Bob") val slot = slot<User>() every { repository.save(capture(slot)) } answers { slot.captured.copy(id = 1) } // When val saved = service.createUser(user) // Then saved.id shouldBe 1 saved.name shouldBe "Bob" verify(exactly = 1) { repository.save(any()) } } } // Relaxed mocks - return default values val mock = mockk<UserRepository>(relaxed = true) // Spy - partial mocking val spy = spyk(RealUserService()) every { spy.generateId() } returns 42 // Mock extension functions mockkStatic(String::toUpperCase) every { "hello".toUpperCase() } returns "MOCKED" // Mock objects object Logger { fun log(message: String) = println(message) } mockkObject(Logger) every { Logger.log(any()) } just Runs verify { Logger.log("test") }
13.5 Coroutine Testing
import kotlinx.coroutines.* import kotlinx.coroutines.test.* import org.junit.jupiter.api.Test class CoroutineServiceTest { @Test fun `should fetch user data`() = runTest { val service = UserService() val user = service.fetchUser(1) user shouldNotBe null user.name shouldBe "Alice" } @Test fun `should handle concurrent requests`() = runTest { val service = UserService() val users = (1..10).map { id -> async { service.fetchUser(id) } }.awaitAll() users.size shouldBe 10 users.all { it != null }.shouldBeTrue() } @Test fun `should timeout on slow operations`() = runTest { val service = SlowService() shouldThrow<TimeoutCancellationException> { withTimeout(1000) { service.verySlowOperation() } } } @Test fun `should advance time virtually`() = runTest { val service = TimedService() // Virtual time - no actual delay delay(5000) val result = service.checkStatus() result shouldBe "Ready" } @Test fun `should test flow emissions`() = runTest { val flow = flowOf(1, 2, 3, 4, 5) val results = mutableListOf<Int>() flow.collect { results.add(it) } results shouldContainExactly listOf(1, 2, 3, 4, 5) } @Test fun `should test flow transformations`() = runTest { val flow = (1..5).asFlow() .map { it * 2 } .filter { it > 5 } val results = flow.toList() results shouldContainExactly listOf(6, 8, 10) } @Test fun `should use test dispatcher`() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val scope = CoroutineScope(dispatcher) var completed = false scope.launch { delay(1000) completed = true } // Not completed yet - virtual time hasn't advanced completed.shouldBeFalse() // Advance virtual time advanceTimeBy(1000) // Now completed completed.shouldBeTrue() } } // Testing suspending functions class SuspendFunctionTest { @Test fun `should test suspend function`() = runTest { suspend fun fetchData(): String { delay(100) return "data" } val result = fetchData() result shouldBe "data" } @Test fun `should verify mock suspend calls`() = runTest { val repository = mockk<UserRepository>() coEvery { repository.fetchUser(any()) } returns User(1, "Alice") val user = repository.fetchUser(1) user.name shouldBe "Alice" coVerify { repository.fetchUser(1) } } }
13.6 kotlin.test Assertions
import kotlin.test.* class KotlinTestExample { @Test fun `should use kotlin test assertions`() { // Equality assertEquals(5, 2 + 3) assertNotEquals(4, 2 + 3) // Boolean assertTrue(2 + 2 == 4) assertFalse(2 + 2 == 5) // Null checks assertNull(null) assertNotNull("value") // Exceptions assertFails { throw IllegalStateException() } assertFailsWith<IllegalArgumentException> { require(false) { "Validation failed" } } // Content equality assertContentEquals(listOf(1, 2, 3), listOf(1, 2, 3)) // Custom message assertEquals(5, 2 + 3, "Addition should work") } }
13.7 Property-Based Testing with Kotest
import io.kotest.core.spec.style.StringSpec import io.kotest.property.* import io.kotest.property.arbitrary.* class PropertyBasedTest : StringSpec({ "string length is never negative" { checkAll<String> { str -> str.length shouldBeGreaterThanOrEqual 0 } } "reversing a list twice returns original" { checkAll<List<Int>> { list -> list.reversed().reversed() shouldBe list } } "addition is commutative" { checkAll<Int, Int> { a, b -> a + b shouldBe b + a } } "custom generators" { val emails = arbitrary { val name = Arb.string(5..10, Codepoint.alphanumeric()).bind() val domain = Arb.string(5..10, Codepoint.alphanumeric()).bind() "$name@$domain.com" } checkAll(emails) { email -> email shouldContain "@" email shouldEndWith ".com" } } "edge cases" { val config = PropTestConfig(iterations = 1000) checkAll(config, Arb.int()) { n -> val doubled = n * 2 if (n >= 0) { doubled shouldBeGreaterThanOrEqual n } else { doubled shouldBeLessThanOrEqual n } } } })
13.8 Test DSL Patterns
// Custom test DSL class TestDsl { fun scenario(name: String, block: ScenarioContext.() -> Unit) { println("Scenario: $name") ScenarioContext().block() } } class ScenarioContext { fun given(description: String, block: () -> Unit) { println(" Given $description") block() } fun whenever(description: String, block: () -> Unit) { println(" When $description") block() } fun then(description: String, block: () -> Unit) { println(" Then $description") block() } } // Usage class UserFlowTest : StringSpec({ "user registration flow" { scenario("new user signs up") { var user: User? = null given("valid user details") { user = User(name = "Alice", email = "alice@example.com") } whenever("user submits registration") { user = userService.register(user!!) } then("user should receive confirmation email") { emailService.wasSent(user!!.email).shouldBeTrue() } then("user should have active account") { user!!.isActive.shouldBeTrue() } } } }) // Fixture DSL fun testWithUser(block: suspend (User) -> Unit) = runTest { val user = createTestUser() try { block(user) } finally { cleanup(user) } } // Usage @Test fun `should update user profile`() = testWithUser { user -> val updated = service.updateProfile(user.id, "New Name") updated.name shouldBe "New Name" } // Data builders with DSL fun user(block: UserBuilder.() -> Unit): User { return UserBuilder().apply(block).build() } class UserBuilder { var name: String = "Test User" var email: String = "test@example.com" var age: Int = 30 var roles: List<String> = emptyList() fun build() = User(name, email, age, roles) } // Usage in tests @Test fun `should validate admin user`() { val admin = user { name = "Admin" email = "admin@example.com" roles = listOf("ADMIN", "USER") } admin.isAdmin().shouldBeTrue() }
13.9 Integration Testing Patterns
import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers @Testcontainers class DatabaseIntegrationTest { companion object { @Container val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply { withDatabaseName("testdb") withUsername("test") withPassword("test") } } @Test fun `should persist and retrieve user`() { val repository = UserRepository(postgres.jdbcUrl) val user = User(name = "Alice", email = "alice@example.com") val saved = repository.save(user) val found = repository.findById(saved.id) found shouldBe saved } } // HTTP client testing @Test fun `should call external API`() = runTest { val mockServer = MockWebServer() mockServer.enqueue( MockResponse() .setBody("""{"name": "Alice"}""") .setHeader("Content-Type", "application/json") ) val client = HttpClient(mockServer.url("/")) val user = client.fetchUser(1) user.name shouldBe "Alice" mockServer.shutdown() }
13.10 Test Organization Best Practices
// Nested tests for organization @Nested @DisplayName("User validation") inner class UserValidation { @Test fun `should reject null name`() { shouldThrow<IllegalArgumentException> { User(name = null, email = "test@example.com") } } @Nested @DisplayName("Email validation") inner class EmailValidation { @Test fun `should reject invalid email format`() { shouldThrow<IllegalArgumentException> { User(name = "Alice", email = "invalid") } } @Test fun `should accept valid email`() { val user = User(name = "Alice", email = "alice@example.com") user.email shouldBe "alice@example.com" } } } // Tags for filtering tests @Tag("integration") @Tag("slow") class SlowIntegrationTest { @Test fun `should complete long operation`() { // Long running test } } // Conditional test execution @EnabledIf("customCondition") fun customCondition(): Boolean = System.getenv("RUN_INTEGRATION") == "true" @Test fun `should run only in CI`() { // Only runs when condition is true }
16. Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- patterns-concurrency-dev: Coroutines, Flow, channels, structured concurrency
- patterns-serialization-dev: kotlinx.serialization, Jackson with Kotlin
- patterns-metaprogramming-dev: Annotations, reflection, DSL builders
- patterns-testing-dev: Test frameworks, mocking, property-based testing
17. Further Resources
Specialized Skills
- lang-kotlin-coroutines-eng: Advanced Flow, channels, structured concurrency
- lang-kotlin-library-dev: Publishing libraries, API design
- lang-kotlin-patterns-eng: Design patterns and architectural patterns
- lang-kotlin-multiplatform-dev: Kotlin Multiplatform (KMP)
Official Documentation
- Kotlin Language Reference: https://kotlinlang.org/docs/reference/
- Kotlin Coroutines Guide: https://kotlinlang.org/docs/coroutines-guide.html
- Kotlin Standard Library: https://kotlinlang.org/api/latest/jvm/stdlib/
Style Guides
- Kotlin Coding Conventions: https://kotlinlang.org/docs/coding-conventions.html
- Android Kotlin Style Guide: https://developer.android.com/kotlin/style-guide