Basics
Variables & Types
Prefer val (immutable) over var (mutable). Kotlin infers types but allows explicit annotations. Use 'is' for type checks (smart-casts automatically). Everything is an object—no primitives in Kotlin syntax.
val name = "Alice" // immutable (preferred)
var age = 30 // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List<Int> = listOf(1, 2, 3)
println(name::class) // class kotlin.String
println(age is Int) // trueString Templates
$var for simple variables, ${expr} for expressions. Triple-quoted strings preserve newlines—use trimIndent() for clean formatting. String templates make string building concise and readable without format strings.
val name = "Alice"
val age = 30
println("Name: $name, Age: $age") // simple var
println("Length: ${name.length}") // expression
println("Upper: ${name.uppercase()}")
val multiline = """
Hello, $name
Age: $age
""".trimIndent()Null Safety
Kotlin's null safety: ? marks nullable types, ?. safe call (returns null if null), !! asserts non-null (throws NPE if null), ?: elvis (default if null). This eliminates NullPointerException at compile time—core Kotlin feature.
var name: String? = null // nullable type
println(name?.length) // null (safe call)
name = "Alice"
println(name!!.length) // 5 (non-null assertion)
val len: Int = name?.length ?: 0 // elvis operator
// Non-null types can never be null
val s: String = "hi" // not nullableType Conversion
Kotlin requires explicit type conversion (no implicit widening like Java). Use toInt()/toDouble()/toString() for conversion. toIntOrNull() returns null on failure—safer than toInt() which throws. Always handle null from parsing.
val n: Int = 42
val d: Double = n.toDouble()
val s: String = n.toString()
val fromStr: Int? = "100".toIntOrNull()
val invalid: Int? = "abc".toIntOrNull() // null
println(d, s, fromStr, invalid)Type Checks & Smart Cast
is checks type and smart-casts the variable in that branch—no explicit cast needed. !is is the negation. Use as for unsafe casts (throws ClassCastException), as? for safe casts (returns null on failure). Smart cast is a major Kotlin convenience.
fun describe(x: Any): String =
when (x) {
is Int -> "Int: ${x + 1}" // x smart-cast to Int
is String -> "String of length ${x.length}"
is List<*> -> "List with ${x.size} items"
else -> "Unknown"
}
println(describe(42)) // Int: 43
println(describe("hello")) // String of length 5Strings
Common String Methods
Kotlin strings have rich extension functions from the standard library. Most return new strings (immutable). Use these instead of manual loops. Functions like uppercase() are locale-aware—use uppercase(Locale.ROOT) for consistent results.
val s = "Hello, World"
println(s.length) // 12
println(s.uppercase()) // HELLO, WORLD
println(s.lowercase()) // hello, world
println(s.split(", ")) // [Hello, World]
println(s.replace("o", "0")) // Hell0, W0rld
println(s.reversed()) // dlroW ,olleH
println(s.startsWith("Hello")) // trueMultiline & Raw Strings
Triple-quoted strings are raw—no escape sequences needed (except triple double-quotes). trimMargin('|') keeps only text after | for clean indentation. trimIndent() removes common whitespace. Ideal for SQL, JSON, HTML in code.
val text = """
|Hello,
|World!
""".trimMargin() // | marks line start
println(text)
val json = """{"name": "Alice", "age": 30}"""
println(json) // no escape needed for quotes
// trimIndent() removes common leading whitespaceString Building
joinToString is the idiomatic way to join collections with separators, prefix, postfix. buildString provides a StringBuilder scope for concise building. Avoid repeated + in loops—use joinToString or StringBuilder for efficiency.
val parts = listOf("apple", "banana", "cherry")
println(parts.joinToString(", ")) // apple, banana, cherry
println(parts.joinToString(prefix="[", postfix="]"))
val sb = StringBuilder()
for (p in parts) sb.append(p).append(" ")
println(sb.toString().trim())
// buildString for concise building
val result = buildString {
parts.forEach { append(it).append(";") }
}String to Number
toInt()/toDouble() throw NumberFormatException on invalid input. toIntOrNull() returns null—pair with ?: for safe defaults. toBooleanStrict() only accepts 'true'/'false'. Use OrNull variants for user input or untrusted data.
val n = "42".toInt() // 42 (throws if invalid)
val d = "3.14".toDouble() // 3.14
val safe = "abc".toIntOrNull() // null
val withDefault = "x".toIntOrNull() ?: 0 // 0
val bool = "true".toBooleanStrict() // true
println(n, d, safe, withDefault)Regex
Regex class wraps Java's Pattern. find returns MatchResult? (first match), findAll returns all. matches checks full match, containsMatchIn checks partial. Use triple-quoted strings for patterns to avoid double-escaping backslashes.
val email = Regex("[\w.]+@[\w]+\.[a-z]+")
val text = "Contact: [email protected]"
val match = email.find(text)
println(match?.value) // [email protected]
println(email.matches("[email protected]")) // true
val replaced = text.replace(Regex("\d+"), "#")
println(email.containsMatchIn(text)) // trueData Structures
List
listOf creates immutable lists, mutableListOf for mutable. Lists are zero-indexed. Use first()/last() for ends (throw on empty), firstOrNull() for safety. contains checks membership. Prefer immutable lists unless you need to modify.
val nums = listOf(1, 2, 3) // immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable[0] = 0
println(nums.size) // 3
println(nums.first()) // 1
println(nums.last()) // 3
println(nums.contains(2)) // true
println(nums.indexOf(2)) // 1Map
mapOf creates immutable maps, mutableMapOf for mutable. 'to' infix creates pairs. [] access returns nullable V? (null if missing). Use getOrDefault or getValue (throws if missing). Iterate with destructuring (k, v).
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"]) // 30
println(ages.getOrDefault("Eve", 0)) // 0
println(ages.containsKey("Alice")) // true
for ((k, v) in ages) println("$k: $v")
val mutable = mutableMapOf("a" to 1)
mutable["b"] = 2 // add/updateSet
setOf creates immutable sets, mutableSetOf for mutable. union/intersect/subtract return new sets. Sets enforce uniqueness—add returns false if element exists. Use for deduplication and set operations. LinkedHashSet preserves insertion order.
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b) // [1,2,3,4,5]
println(a intersect b) // [3]
println(a subtract b) // [1,2]
println(a.contains(2)) // true
val mutable = mutableSetOf(1, 2)
mutable.add(3) // true if addedArray & Primitive Arrays
arrayOf creates Array<T> (boxed for primitives). Use intArrayOf/doubleArrayOf for unboxed primitive arrays (better performance). Array(size) { init } creates with an init function. Arrays are mutable; prefer List for most use cases.
val arr = arrayOf(1, 2, 3) // Array<Int>
arr[0] = 10
println(arr.size) // 3
// Specialized arrays for primitives (no boxing)
val ints = intArrayOf(1, 2, 3)
val doubles = doubleArrayOf(1.0, 2.0)
// Array constructor
val squares = Array(5) { it * it } // [0,1,4,9,16]
println(squares.toList())Pair & Triple
Pair and Triple group 2-3 values. 'to' infix creates Pairs (common for maps). Destructure with val (a, b) = pair. For more than 3 values or named fields, use data classes—they're clearer and more maintainable.
val pair = "Alice" to 30 // Pair<String, Int>
println(pair.first) // Alice
println(pair.second) // 30
val (name, age) = pair // destructuring
val triple = Triple(1, "a", 2.0)
println(triple.first, triple.third)
// Useful for returning two values
fun minMax(list: List<Int>): Pair<Int, Int> =
Pair(list.min(), list.max())Control Flow
If / Else (Expression)
In Kotlin, if/else is an expression that returns a value. This replaces the ternary operator. Both branches must return compatible types. Use for concise conditional assignment. As a statement (no else), it returns Unit.
val score = 85
val grade = if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else "F"
println(grade) // B
// if returns a value—no ternary needed
// Both branches must have compatible typesWhen (Switch)
when is Kotlin's powerful switch—supports literals, ranges (in), negation (!in), multiple values (comma), and is-checks. Must be exhaustive when used as expression with sealed types. Without a subject, it's a cleaner if/else chain.
val n = 2
val label = when (n) {
0 -> "zero"
1, 2, 3 -> "small"
in 4..10 -> "medium"
!in 1..100 -> "out of range"
else -> "large"
}
println(label) // small
// When without subject = multi-condition if
when {
n > 0 -> println("positive")
n < 0 -> println("negative")
else -> println("zero")
}For Loops & Ranges
.. is inclusive range, until is exclusive, downTo counts down, step sets increment. withIndex() gives indexed iteration. for-in works with any Iterable. These ranges are concise and readable for counting loops.
for (i in 0 until 5) print(i) // 01234 (excludes 5)
for (i in 1..5) print(i) // 12345 (includes 5)
for (i in 5 downTo 1) print(i) // 54321
for (i in 1..10 step 2) print(i) // 13579
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
println("$index: $value")
}While & Do-While
while checks before, do-while checks after (runs at least once). Both require mutable state. Prefer for loops or functional operations (map, filter) for immutability. Use while for condition-driven loops where the count isn't known.
var count = 0
while (count < 3) {
println(count)
count++
}
var j = 0
do {
println(j)
j++
} while (j < 3)
// do-while runs at least once
// Prefer for loops when possibleBreak & Continue (Labels)
continue skips to next iteration, break exits the loop. Labels (@outer) enable breaking out of nested loops—define with label@ before the loop, break/continue with @label. Use sparingly; refactoring to functions is often cleaner.
for (i in 1..5) {
if (i == 3) continue // skip 3
if (i == 5) break // stop at 5
println(i) // 1, 2, 4
}
// Labeled breaks for nested loops
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i == 2 && j == 2) break@outer
println("$i,$j")
}
}Functions & Lambdas
Function Definition
Functions use fun name(params): ReturnType. Expression body (= expr) for single expressions. Default params and named arguments supported. Unit return type = side effect only. Default params reduce the need for overloading.
fun add(a: Int, b: Int): Int = a + b // expression body
fun greet(name: String, greeting: String = "Hello"): String {
return "$greeting, $name!"
}
println(add(3, 4)) // 7
println(greet("Alice")) // Hello, Alice!
println(greet("Bob", greeting = "Hi")) // named arg
fun log(msg: String): Unit = println(msg) // Unit = voidLambdas & Higher-Order
Lambdas: { params -> body }. 'it' is shorthand for a single parameter. Pass lambdas to higher-order functions like map/filter/reduce. Trailing lambda syntax: if the last param is a function, it can go outside (). This is idiomatic Kotlin.
val square: (Int) -> Int = { x -> x * x }
println(square(5)) // 25
val nums = listOf(1, 2, 3)
println(nums.map { it * 2 }) // [2, 4, 6]
println(nums.filter { it > 1 }) // [2, 3]
println(nums.reduce { a, b -> a + b }) // 6
// 'it' is the implicit single parameterExtension Functions
Extension functions add methods to existing classes (even from Java) without inheritance or modification. They're syntactic sugar for static functions. 'this' refers to the receiver. Use to make APIs fluent. Resolved at compile time (no dynamic dispatch).
fun String.shout(): String = this.uppercase() + "!"
fun Int.isEven(): Boolean = this % 2 == 0
println("hello".shout()) // HELLO!
println(4.isEven()) // true
// Extensions don't modify the class—they're resolved statically
// Use to add utility methods to any typeInline & Infix
inline copies the function body to call sites (eliminates lambda overhead). Use for higher-order functions in hot paths. infix enables function calls without dot/parens (a op b)—use for DSLs and readable operations like 'to', 'in', 'and'.
inline fun measure(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
println("Took ${System.currentTimeMillis() - start}ms")
}
measure { println("working...") }
// Infix functions for readable calls
infix fun Int.times(str: String): String = str.repeat(this)
println(3 times "ab") // abababVararg & Spread
vararg accepts variable arguments (collected into an array). * spreads an array into vararg. Named params can follow vararg. Use for flexible APIs like listOf(), printf-style functions. The spread operator is Kotlin's equivalent of JS spread.
fun sum(vararg nums: Int): Int = nums.sum()
println(sum(1, 2, 3, 4)) // 10
// Spread operator to pass array
val arr = intArrayOf(1, 2, 3)
println(sum(*arr)) // 6
// Named/default params can follow vararg
fun greet(vararg names: String, greeting: String = "Hi") =
names.joinToString { "$greeting, $it!" }
println(greet("A", "B"))Classes & OOP
Class & Constructor
Primary constructor is in the class header. val/var params become properties (with getters/setters). Without val/var, they're just constructor params (not accessible). The init block runs during construction. Kotlin generates getters/setters automatically.
class Person(val name: String, val age: Int) {
fun greet(): String = "Hi, I'm $name"
fun isAdult(): Boolean = age >= 18
}
val p = Person("Alice", 30)
println(p.greet()) // Hi, I'm Alice
println(p.name) // Alice (val = property)
println(p.isAdult()) // true
// val/var in constructor = auto propertyData Class
data class auto-generates equals, hashCode, toString, copy, and componentN (for destructuring). Use for data holders. Must have at least one val/var param. copy() creates a modified clone—great for immutable updates. Destructure with val (a, b) = point.
data class Point(val x: Int, val y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2) // true (value equality)
println(p1.copy(x = 5)) // Point(x=5, y=4)
println(p1) // Point(x=3, y=4)
// Auto: equals, hashCode, toString, copy, componentN
val (x, y) = p1 // destructuring via componentN()Sealed Class
Sealed classes restrict subtypes to the same file/kotlin module. when expressions are exhaustive (compiler checks all cases). Use for ADTs (Algebraic Data Types) representing finite states. Combined with when, this enables safe, compile-checked pattern matching.
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
object Loading : Result()
fun handle(r: Result): String = when (r) {
is Success -> "Got ${r.value}"
is Failure -> "Error: ${r.error}"
Loading -> "Loading..."
}
// when is exhaustive—compiler enforces all casesObject & Companion
object declares a singleton (one instance, lazily initialized). companion object inside a class holds 'static' members (accessed via ClassName.member). Use object for singletons, companion for factory methods and constants. Companion can implement interfaces.
object Config { // singleton
val version = "1.0"
fun load() = mapOf("key" to "value")
}
println(Config.version) // 1.0
class User(val name: String) {
companion object {
fun create(name: String) = User(name)
const val MAX = 100
}
}
val u = User.create("Alice") // like static methodInheritance & Interface
Classes are final by default—use 'open' to allow inheritance. 'override' is required. Interfaces can have default implementations. A class extends one class but implements multiple interfaces. Use abstract for partial implementation. Prefer composition over inheritance.
open class Animal(val name: String) {
open fun speak() = "..."
}
class Dog(name: String) : Animal(name) {
override fun speak() = "Woof"
}
interface Greetable {
val name: String
fun greet(): String // can have default impl
}
class Person(override val name: String) : Greetable {
override fun greet() = "Hi, $name"
}Null Safety & Scope Functions
Safe Calls & Elvis
?. safe call (returns null if receiver is null), ?: elvis (provides default). Chain ?. for deep access (user?.address?.city). Use ?: return/throw for early exits. These make null handling concise and safe—no explicit null checks needed.
var name: String? = "Alice"
println(name?.length) // 5
name = null
println(name?.length) // null
println(name?.length ?: 0) // 0 (elvis: default if null)
println(name?.length ?: return) // early return
// Safe call chain
val city: String? = user?.address?.citylet (Null Check)
let executes a block only if the value is non-null. 'it' is the non-null value inside the block. Use for null-guarded operations instead of if-null checks. Common pattern: value?.let { ... } for safe processing. Returns the block's result.
var name: String? = "Alice"
name?.let {
println("Name is $it") // only runs if not null
println("Length: ${it.length}") // it is non-null
}
// Common for null-guarded blocks
val result = name?.let {
process(it) // it: String (non-null)
} ?: "default"apply & also
apply configures an object (returns the object, 'this' receiver)—ideal for builders. also performs side effects (returns the object, 'it' param)—good for logging/debugging in chains. Both return the original object, enabling fluent chains.
val list = mutableListOf<Int>().apply {
add(1); add(2); add(3) // 'this' = the list
println("Built $size items")
}
val config = Config().also {
it.timeout = 30 // 'it' = config
println("Configured")
}
// apply: configure and return object (this)
// also: side effects, return object (it)run & with
run executes a block on an object (returns block result, 'this' receiver)—use for transforming an object. with is like run but takes the object as a parameter (not chainable on nullables). Use for grouping operations on the same object.
val result = "Hello".run {
length // 'this' = string, returns last expr
} // 5
val r2 = with(StringBuilder()) {
append("a"); append("b")
toString() // returns this
}
// run: object.run { } - returns block result
// with: with(obj) { } - returns block result (not chainable)TakeIf & TakeUnless
takeIf returns the object if the predicate is true, null otherwise. takeUnless is the opposite. Use for conditional filtering in chains—avoids separate if checks. Combine with ?: for defaults. Elegant for validation pipelines.
val age = 25
val valid = age.takeIf { it >= 18 } // 25 (if true)
val invalid = age.takeIf { it < 18 } // null (if false)
val adult = age.takeUnless { it < 18 } // 25
// Useful for filtering in chains
val email = getUser()?.email?.takeIf { it.contains("@") }
println(valid, invalid, adult)Collections & Functional
Map / Filter / Fold
map transforms, filter selects, reduce/fold aggregate. 'it' is the implicit element. fold takes a seed; reduce doesn't (throws on empty). These are the core of functional collection processing—use instead of loops for clarity.
val nums = listOf(1, 2, 3, 4, 5)
println(nums.map { it * 2 }) // [2,4,6,8,10]
println(nums.filter { it % 2 == 0 }) // [2,4]
println(nums.reduce { a, b -> a + b }) // 15
println(nums.fold(0) { a, b -> a + b }) // 15
println(nums.sum()) // 15
println(nums.joinToString(", ")) // 1, 2, 3, 4, 5FlatMap & GroupBy
flatten removes one level of nesting. flatMap maps and flattens in one step—essential for nested transformations. groupBy partitions by a key into a Map. These are powerful for data processing and analysis pipelines.
val nested = listOf(listOf(1, 2), listOf(3, 4))
println(nested.flatten()) // [1,2,3,4]
println(nested.flatMap { it.map { n -> n * 2 } }) // [2,4,6,8]
val words = listOf("apple", "bat", "ant")
val byFirst = words.groupBy { it.first() }
// {a=[apple, ant], b=[bat]}
println(byFirst)Sorting
sorted/sortedDescending sort naturally. sortedBy/sortedByDescending sort by a key selector. These return new lists (immutable). For mutable lists, use sort/sortBy (in-place). Use key selectors for sorting by a specific field.
val nums = listOf(3, 1, 4, 1, 5)
println(nums.sorted()) // [1,1,3,4,5]
println(nums.sortedDescending()) // [5,4,3,1,1]
val people = listOf("Alice" to 30, "Bob" to 25)
val byAge = people.sortedBy { it.second } // by age
println(byAge)
val byNameDesc = people.sortedByDescending { it.first }
println(byNameDesc)Sequences (Lazy)
Sequences are lazy—operations are deferred until a terminal operation (toList, sum, count). Avoids intermediate collections for better performance on large data. Use asSequence() for multi-step pipelines on big collections. Like Java Streams.
val nums = (1..1000000).toList()
// Eager: creates intermediate lists
val eager = nums.filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Lazy: no intermediate collections
val lazy = nums.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
.toList() // forces evaluation
println(lazy) // [4, 8, 12, 16, 20]Partition & Chunked
partition splits into two lists by a predicate (returns Pair). chunked divides into fixed-size lists. windowed creates sliding windows. These are useful for batching, pagination, and sliding-window algorithms. All return new collections.
val nums = listOf(1, 2, 3, 4, 5)
val (evens, odds) = nums.partition { it % 2 == 0 }
println(evens) // [2, 4]
println(odds) // [1, 3, 5]
val chunked = nums.chunked(2)
println(chunked) // [[1, 2], [3, 4], [5]]
val windowed = nums.windowed(3)
println(windowed) // [[1,2,3], [2,3,4], [3,4,5]]Coroutines & Async
Launch (Fire & Forget)
launch starts a coroutine that doesn't return a result (fire-and-forget). delay is non-blocking (unlike Thread.sleep). runBlocking bridges sync/async code (use in main/tests). Coroutines are lightweight—thousands can run on a few threads.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000)
println("World!") // after 1s
}
println("Hello") // immediately
// launch doesn't return a result
// Use for side-effect coroutines
}
// Output: Hello, then World! after 1sAsync & Await
async starts a coroutine that returns a Deferred<T>. await() suspends until the result is ready. Start multiple asyncs before awaiting for parallelism. Use for concurrent computations that produce results. Like Promise/Future in other languages.
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async {
delay(1000)
42 // return value
}
val result = deferred.await()
println(result) // 42
// Parallel execution
val a = async { computeA() }
val b = async { computeB() }
println(a.await() + b.await()) // concurrent
}Suspend Functions
suspend functions can pause and resume without blocking threads. They can only be called from coroutines or other suspend functions. coroutineScope provides a structured scope (waits for all children). Use for async APIs—makes async code look synchronous.
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): String {
delay(500) // simulates network
return "User $id"
}
suspend fun fetchAll(): List<String> = coroutineScope {
val a = async { fetchUser(1) }
val b = async { fetchUser(2) }
listOf(a.await(), b.await())
}
// suspend functions can only be called from coroutinesFlow (Cold Stream)
Flow is Kotlin's cold async stream (like RxJava Observable). Values are produced on collection. Use map/filter/reduce operators. emit produces, collect consumes. Ideal for streaming data, events, or paginated APIs. Hot streams use SharedFlow/StateFlow.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i) // produce value
}
}
fun main() = runBlocking {
numbers().collect { println(it) } // 1,2,3,4,5
// Transform
numbers().map { it * 2 }.filter { it > 4 }.collect {
println(it) // 6, 8, 10
}
}Dispatchers & Context
Dispatchers choose the thread pool: Main for UI, IO for network/file (large pool), Default for CPU work (cores count). withContext switches context within a coroutine. Use the right dispatcher to avoid blocking UI or starving thread pools.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch(Dispatchers.Main) { /* UI thread */ }
launch(Dispatchers.IO) {
// Network/file I/O (thread pool)
val data = fetchData()
}
launch(Dispatchers.Default) {
// CPU-intensive work
val result = heavyCompute()
}
// withContext switches dispatcher
val data = withContext(Dispatchers.IO) { readFile() }
}Error Handling & I/O
Try / Catch / Finally
try/catch/finally is like Java but try is an expression returning a value. No checked exceptions—all are unchecked. Catch specific exceptions for targeted handling. Use finally for cleanup. Prefer Result or nullable returns for expected failures.
val result = try {
"abc".toInt()
} catch (e: NumberFormatException) {
0 // fallback
} finally {
println("cleanup")
}
println(result) // 0
// try is an expression—returns a value
// Kotlin doesn't have checked exceptionsResult Type
Result<T> wraps success or failure (like Try in Scala). runCatching converts exceptions to Result. getOrNull/getOrElse for safe access. onSuccess/onFailure for callbacks. Use for expected errors instead of exceptions—cleaner functional error handling.
fun parse(s: String): Result<Int> =
runCatching { s.toInt() }
val r = parse("42")
println(r.getOrNull()) // 42
println(r.getOrElse { 0 }) // 42
r.onSuccess { println("OK: $it") }
.onFailure { println("Err: ${it.message}") }
val r2 = parse("abc")
println(r2.getOrNull()) // nullCustom Exceptions
Custom exceptions extend Exception (or a subclass). Pass a message for debugging. Catch specific exceptions before generic ones (order matters). Use exceptions for truly exceptional cases; for expected failures, prefer Result or nullable returns.
class InvalidAgeException(message: String) : Exception(message)
fun setAge(age: Int) {
if (age < 0) throw InvalidAgeException("Age cannot be negative: $age")
}
try {
setAge(-5)
} catch (e: InvalidAgeException) {
println("Caught: ${e.message}")
} catch (e: Exception) {
println("Other: ${e.message}")
}File I/O
Kotlin uses Java's File with extension functions. writeText/readText for simple cases. useLines streams lines (auto-closes, memory-efficient). For large files, use bufferedReader(). Always close resources—use use { } block for auto-closing.
import java.io.File
// Write
File("test.txt").writeText("Hello, File!")
// Read
val content = File("test.txt").readText()
println(content) // Hello, File!
// Append
File("log.txt").appendText("new line\n")
// Line by line
File("test.txt").useLines { lines ->
lines.forEach { println(it) }
}JSON (kotlinx.serialization)
kotlinx.serialization is Kotlin's official JSON library—compile-time safe, no reflection. Annotate data classes with @Serializable. encodeToString/decodeFromString for JSON. Use @SerialName for custom key names, @Optional for defaults. Alternative: Gson/Moshi.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user)
println(json) // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name) // Alice
// Requires @Serializable annotationCoroutines Deep Dive
Structured Concurrency
Structured concurrency ties child coroutines to a parent scope—the parent won't complete until all children do, and a child failure cancels siblings. Prefer coroutineScope over GlobalScope. This prevents coroutine leaks and makes cancellation predictable.
import kotlinx.coroutines.*
fun main() = runBlocking {
// coroutineScope waits for all children
coroutineScope {
launch { delay(1000); println("A") }
launch { delay(500); println("B") }
}
println("Done") // after both complete
// If a child fails, parent cancels siblings
// Never use GlobalScope unless necessary
}Cancellation & Cooperative Cancellation
Cancellation is cooperative—coroutines must check for it at suspension points (delay, yield, await). Cancellable suspending functions throw CancellationException. Use ensureActive() or isActive to check. Non-suspending CPU loops won't respond to cancellation unless you call yield() or check isActive.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(10) { i ->
delay(300)
yield() // explicit suspension point
println("Working $i")
}
}
delay(700)
job.cancelAndJoin() // cancels and waits
println("Cancelled")
}
// delay/yield check for cancellation
// isActive checks cancellation stateException Handling
Unhandled exceptions in launch propagate to the parent (cancelling siblings) unless a CoroutineExceptionHandler is installed. async defers exceptions to await(). Use try/catch around await. The handler only works for uncaught exceptions in launch—use SupervisorJob to isolate failures.
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("Caught: $e")
}
val job = launch(handler) {
throw RuntimeException("Boom!")
}
job.join()
// async exceptions surface on await()
val deferred = async { throw IllegalStateException() }
try { deferred.await() } catch (e: Exception) {
println("Async failed: $e")
}
}Supervision (supervisorScope)
supervisorScope (and SupervisorJob) creates a scope where a child's failure does NOT cancel its siblings—each child fails independently. Use it for independent operations (e.g., multiple independent API calls). Regular coroutineScope cancels siblings on first failure (fail-fast).
import kotlinx.coroutines.*
fun main() = runBlocking {
// supervisorScope: child failures don't cancel siblings
supervisorScope {
launch { delay(100); throw RuntimeException("A fails") }
launch { delay(200); println("B still runs") }
}
// vs coroutineScope: A's failure cancels B
// Use SupervisorJob for long-lived services
val scope = CoroutineScope(SupervisorJob())
}Channels (CSP-style)
Channels pass values between coroutines (like Go channels). Rendezvous (capacity 0) synchronizes sender and receiver; buffered allows queuing. send suspends when full, receive when empty. Always close() producers. For fan-out, use BroadcastChannel or SharedFlow. Prefer Flow for most streaming needs.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
val channel = Channel<Int>(3) // buffered capacity 3
launch {
for (i in 1..5) {
channel.send(i)
println("Sent $i")
}
channel.close()
}
// receive() blocks until available
for (x in channel) println("Got $x")
// Produce-consume pattern
// Rendezvous (cap 0) syncs sender/receiver
}Flow Deep Dive
Flow Operators
Flow operators are cold—they execute only when collected. map/filter/take are like Stream/Sequence. transform is the most flexible (can emit multiple values per input). flatMap variants handle nested flows: concat (sequential), merge (concurrent), latest (cancel previous). Choose based on ordering/concurrency needs.
import kotlinx.coroutines.flow.*
fun nums() = flow {
for (i in 1..5) emit(i)
}
suspend fun main() {
nums().map { it * it } // 1,4,9,16,25
.filter { it > 5 } // 9,16,25
.take(2) // 9,16
.collect { println(it) }
// transform: emit multiple values
nums().transform { x ->
emit(x)
emit(x * 10)
}.collect { println(it) }
// flatMapConcat / flatMapMerge / flatMapLatest
}Buffer & Concurrency
buffer decouples producer and consumer with a fixed-capacity queue—useful when production and consumption speeds differ. conflate keeps only the latest value (drop intermediates) for UI/state updates. collectLatest cancels the previous collector when a new value arrives—ideal for search-as-you-type.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun events() = flow {
for (i in 1..3) {
delay(100); emit(i)
}
}
fun main() = runBlocking {
// Without buffer: producer & consumer alternate (slow)
events().buffer() // decouple producer/consumer
.collect { delay(200); println(it) }
// conflate: drop intermediate values
events().conflate().collect { println(it) }
// collectLatest: cancel previous collection
events().collectLatest { delay(200); println(it) }
}StateFlow & SharedFlow (Hot Streams)
StateFlow is a hot stream holding a single up-to-date value—use it for UI state (replaces LiveData). It always has a value and conflate-s. SharedFlow is a hot broadcast stream (multiple collectors, no required initial value)—use it for events. SharedFlow with replay=1 behaves like a cached event bus.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
// StateFlow: holds one value, stateful
val state = MutableStateFlow(0)
launch {
state.collect { println("State: $it") }
}
state.value = 1 // update synchronously
state.value = 2
// SharedFlow: broadcasts to multiple collectors
val events = MutableSharedFlow<String>()
launch { events.collect { println("A: $it") } }
launch { events.collect { println("B: $it") } }
events.emit("Hello") // both receive
}Flow Exception Handling
catch handles upstream exceptions and can emit fallback values. It only catches exceptions emitted before it in the chain. retry/retryWhen re-collect the flow on failure (useful for network calls). For downstream exceptions, wrap collect in try/catch. Never catch inside the flow builder—let it propagate.
import kotlinx.coroutines.flow.*
fun numbers() = flow {
emit(1)
throw RuntimeException("fail")
emit(2) // never reached
}
suspend fun main() {
// catch operator (upstream only)
numbers().catch { e -> emit(-1) }
.collect { println(it) } // 1, -1
// retry on failure
numbers().retry(3) { e ->
println("Retry: $e"); true
}.collect { println(it) }
// retryWhen for custom logic
}flowOn & Context
flowOn switches the dispatcher for the upstream flow (producer + operators above it). This is essential when the flow does blocking I/O—wrap it with flowOn(Dispatchers.IO). The downstream (collect) runs on the caller's context. Multiple flowOn calls create separate contexts for each segment.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun diskFlow() = flow {
for (i in 1..3) {
Thread.sleep(100) // blocking I/O
emit(i)
}
}
suspend fun main() {
// flowOn changes upstream context
diskFlow().flowOn(Dispatchers.IO)
.collect { println(it) }
// Without flowOn, runs on collector's dispatcher
// flowOn applies to all operators above it
diskFlow().map { it * 2 }
.flowOn(Dispatchers.IO)
.filter { it > 2 }
.collect { println(it) }
}Sealed Classes & ADTs
Sealed Classes Basics
Sealed classes restrict subclasses to a known set (same file/module). The compiler knows all possible types, enabling exhaustive when expressions without else. Ideal for modeling finite states (Result, UiState, network responses). Combined with data classes, they form Algebraic Data Types.
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun handle(r: Result<Int>) = when (r) {
is Result.Success -> println("Got ${r.value}")
is Result.Failure -> println("Error: ${r.error}")
Result.Loading -> println("Loading...")
}
// All subclasses defined in same file/moduleSealed Interfaces (Kotlin 1.5+)
Sealed interfaces (Kotlin 1.5+) extend sealed to interfaces, allowing a class to implement multiple sealed types—more flexible than sealed classes. Same-file/same-module restriction applies. Useful for domain modeling where a type belongs to multiple categories.
sealed interface Shape {
fun area(): Double
}
data class Circle(val r: Double) : Shape {
override fun area() = Math.PI * r * r
}
data class Square(val side: Double) : Shape {
override fun area() = side * side
}
// Sealed interfaces allow multiple inheritance
sealed interface Clickable { fun click() }
class Button : Shape, Clickable {
override fun area() = 0.0
override fun click() = println("Clicked")
}Exhaustive When
When used as an expression (not statement), the compiler requires all sealed cases—no else needed. Adding a new subclass produces compile errors at unhandled when sites, making refactoring safe. This is the killer feature of sealed classes for state machines and UI rendering.
sealed class NetworkState {
object Loading : NetworkState()
data class Success(val data: String) : NetworkState()
data class Error(val message: String) : NetworkState()
}
fun render(state: NetworkState): String = when (state) {
NetworkState.Loading -> "Spinner"
is NetworkState.Success -> "Data: ${state.data}"
is NetworkState.Error -> "Error: ${state.message}"
// No else needed—compiler enforces all cases
}
// Adding a new subclass causes compile error
// until you handle itADT Modeling (Result/Either)
Sealed classes model sum types (Either/Result) for railway-oriented programming. fold handles both branches. Unlike exceptions, errors are explicit in the type signature. Arrow-kt provides richer Either/Validated. Use for predictable, composable error handling without try/catch.
sealed class Either<out L, out R> {
data class Left<out L>(val value: L) : Either<L, Nothing>()
data class Right<out R>(val value: R) : Either<Nothing, R>()
}
fun <L, R> Either<L, R>.fold(
ifLeft: (L) -> Unit,
ifRight: (R) -> Unit
) = when (this) {
is Either.Left -> ifLeft(value)
is Either.Right -> ifRight(value)
}
fun divide(a: Int, b: Int): Either<String, Int> =
if (b == 0) Either.Left("Divide by zero")
else Either.Right(a / b)Sealed Classes with Recursion
Sealed classes can be recursive, modeling tree structures (ASTs, JSON, expressions). Pattern matching with when + recursion evaluates them elegantly. This is classic functional ADT usage—type-safe, exhaustive, and refactor-friendly. Used in compilers, parsers, and config DSLs.
sealed class Expr {
data class Num(val value: Int) : Expr()
data class Add(val left: Expr, val right: Expr) : Expr()
data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
is Expr.Num -> e.value
is Expr.Add -> eval(e.left) + eval(e.right)
is Expr.Mul -> eval(e.left) * eval(e.right)
}
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
fun main() = println(eval(expr)) // 14Delegated Properties
lazy Delegate
lazy defers initialization until first access and caches the result. Default is thread-safe (double-checked locking). Use for expensive resources (configs, DB connections, singletons) that may not be needed. Pass NONE mode for single-threaded contexts to avoid synchronization overhead.
val heavyConfig: Config by lazy {
println("Initializing...")
loadConfigFromFile() // runs once, first access
}
fun main() {
println("Before access")
println(heavyConfig) // initializes here
println(heavyConfig) // cached, no re-init
}
// lazy is thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// Use LazyThreadSafetyMode.NONE for single-threadedobservable & vetoable
observable fires a callback after each change (logging, side effects). vetoable can reject changes by returning false (validation). Both take an initial value and a lambda. Use for reactive state, validation, or triggering UI updates. For more complex scenarios, use custom delegates.
import kotlin.properties.Delegates
var count: Int by Delegates.observable(0) { _, old, new ->
println("Changed: $old -> $new")
}
var age: Int by Delegates.vetoable(0) { _, old, new ->
new >= 0 // reject negative values
}
fun main() {
count = 1 // prints "Changed: 0 -> 1"
count = 2 // prints "Changed: 1 -> 2"
age = 25 // accepted
age = -5 // rejected, stays 25
}Custom Property Delegate
Custom delegates implement getValue (and setValue for vars) via ReadOnlyProperty/ReadWriteProperty. They encapsulate reusable property behavior (validation, formatting, caching, DB mapping). The thisRef is the owner, prop is the property metadata. Powerful for ORM/serialization frameworks.
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString : ReadWriteProperty<Any?, String> {
private var value: String = ""
override fun getValue(thisRef: Any?, prop: KProperty<*>) = value
override fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
this.value = value.trim()
}
}
class User {
var name: String by TrimmedString()
}
fun main() {
val u = User()
u.name = " Alice "
println(u.name) // "Alice"
}Map-backed Properties
Delegating to a Map lets you bind properties to dynamic keys—useful for parsing JSON, configs, or ORM rows without boilerplate. Property name must match the map key. Use MutableMap for writable properties. This is how kotlinx.serialization and many ORMs work under the hood.
class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
val email: String? by map
}
fun main() {
val u = User(mapOf(
"name" to "Alice",
"age" to 30,
"email" to "[email protected]"
))
println(u.name) // Alice
}
// Mutable version: MutableMap
class MutableUser(map: MutableMap<String, Any?>) {
var name: String by map
}notNull & Singleton Delegate
notNull() is like lateinit but for any type (including primitives) and works with val/var. It throws if accessed before set. For singletons, prefer object (eager) or by lazy (lazy). lateinit is for vars in classes only; notNull delegate is more flexible but has slight overhead.
import kotlin.properties.Delegates
class Service {
// Late-init for non-null, set once
var config: String by Delegates.notNull()
// Throws before initialization
}
// Singleton via object (no delegate needed)
object Database {
val connection = connect()
}
// Or lazy singleton
val db: Database by lazy { Database() }
fun main() {
val s = Service()
// println(s.config) // IllegalStateException
s.config = "prod"
println(s.config) // prod
}DSL Building
Type-Safe Builders
Type-safe builders use function types with receiver (lambda with receiver) to create nested DSLs. The lambda runs in the context of the receiver (this), so you call its methods directly. This is how Kotlin's HTML DSL, Gradle build scripts, and kotlinx.html work—declarative and compile-time checked.
class Table {
private val rows = mutableListOf<Row>()
fun row(init: Row.() -> Unit) {
val r = Row(); r.init(); rows.add(r)
}
fun build() = rows
}
class Row {
private val cells = mutableListOf<String>()
fun cell(text: String) { cells.add(text) }
fun build() = cells
}
fun table(init: Table.() -> Unit) = Table().apply(init).build()
val t = table {
row { cell("A"); cell("B") }
row { cell("C"); cell("D") }
}@DslMarker (Scope Control)
@DslMarker prevents implicit receiver leakage—inside a nested DSL block, you can only call methods of the innermost receiver. Without it, Kotlin would allow calling outer receivers' methods, leading to confusing/buggy DSLs. Annotate all DSL classes with the same marker annotation.
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(b: Body.() -> Unit) { /*...*/ } }
@HtmlDsl
class Body { fun p(text: String) { /*...*/ } }
fun html(init: HTML.() -> Unit) = HTML().apply(init)
// Without @DslMarker, inner 'this' could call outer methods
html {
body {
// p() is unambiguous—Body's method
// body() would be an error (not Body's method)
p("Hello")
}
}Infix Functions
infix functions enable natural-language syntax (a to b, 1 until 10). They must be member or extension functions with a single parameter. Used heavily in DSLs, testing frameworks, and math libraries. Built-in examples: to (Pair), until/step (ranges), and collection operations.
infix fun Int.times(str: String) = str.repeat(this)
fun main() {
println(3 times "ab") // "ababab"
// Equivalent to: 3.times("ab")
// No dot, no parentheses—reads like natural language
}
// Common in testing
infix fun <T> T.shouldEqual(expected: T) =
assert(this == expected)
fun test() { 5 shouldEqual 5 }
// Also: to, until, step, in, etc.Operator Overloading
Operator overloading lets objects use +, -, *, [], (), etc. with natural syntax. Mark functions with operator. Overload sensibly—math types (Vec, Matrix, Money) benefit; arbitrary overloading hurts readability. get/set enable indexing; invoke makes objects callable. Range operators (rangeTo, contains) power for-loops.
data class Vec(val x: Int, val y: Int) {
operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
operator fun minus(o: Vec) = Vec(x - o.x, y - o.y)
operator fun times(s: Int) = Vec(x * s, y * s)
operator fun unaryMinus() = Vec(-x, -y)
operator fun get(i: Int) = when (i) { 0 -> x; 1 -> y; else -> throw IndexOutOfBoundsException() }
}
fun main() {
val a = Vec(1, 2)
val b = Vec(3, 4)
println(a + b) // Vec(x=4, y=6)
println(a * 2) // Vec(x=2, y=4)
println(-a) // Vec(x=-1, y=-2)
println(a[0]) // 1
}Function Types with Receiver
Function types with receiver (A.(B) -> C) let lambdas access the receiver as 'this'. This is the foundation of Kotlin DSLs and scope functions (apply, run, with). apply returns the receiver; run returns the lambda result. Master this to build expressive, type-safe internal DSLs.
// Lambda with receiver: A.(B) -> C
val greet: String.(Int) -> String = { times -> repeat(times) { this + "!" } }
fun main() {
println("Hello".greet(3)) // Hello!!!Hello!!!Hello!!!
// 'this' is the String receiver
}
// Build DSLs with receiver
class StringBuilder {
private val parts = mutableListOf<String>()
fun add(s: String) { parts.add(s) }
}
fun build(init: StringBuilder.() -> Unit) =
StringBuilder().apply(init)
val sb = build { add("a"); add("b") }Testing (JUnit 5, MockK, Turbine)
JUnit 5 Basics
JUnit 5 (Jupiter) is the standard Kotlin testing framework. @Test marks tests; @BeforeEach/@AfterEach run per-test setup/teardown. Use backtick names for readability. @ParameterizedTest + @ValueSource run a test with multiple inputs. assertThrows checks exceptions. @Disabled skips tests.
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
lateinit var calc: Calculator
@BeforeEach fun setup() { calc = Calculator() }
@Test fun `add two numbers`() {
assertEquals(5, calc.add(2, 3))
}
@Test fun `divide by zero throws`() {
assertThrows<ArithmeticException> { calc.divide(10, 0) }
}
@ParameterizedTest
@ValueSource(ints = [1, 2, 3])
fun `positive numbers`(n: Int) { assertTrue(n > 0) }
@Disabled("TODO") @Test fun skip() {}
}MockK (Mocking)
MockK is Kotlin's idiomatic mocking library (handles final classes, coroutines, extension functions). every { } returns/throws stubs; verify { } checks calls. Use relaxed = true to skip stubbing void methods. MockK supports suspend functions natively via coEvery/coVerify—essential for coroutine testing.
import io.mockk.*
interface UserService { fun find(id: Int): String?; fun save(name: String): Int }
class Test {
val mock = mockk<UserService>()
@Test fun test() {
every { mock.find(1) } returns "Alice"
every { mock.find(any()) } returns null
every { mock.save(any()) } throws RuntimeException("fail")
verify { mock.find(1) } // called once
verify(exactly = 2) { mock.find(any()) }
confirmVerified(mock)
}
}
// relaxUnitFun / relaxed = true for void methodsTurbine (Flow Testing)
Turbine is the standard library for testing Flow. test { } collects in a controlled scope; awaitItem() asserts the next emission, awaitComplete()/awaitError() check termination. It handles timing and virtual time correctly. Combine with runTest for coroutine-friendly, fast, deterministic Flow tests.
import app.cash.turbine.test
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FlowTest {
@Test fun testFlow() = runTest {
flowOf(1, 2, 3).test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
awaitComplete()
}
}
@Test fun testError() = runTest {
flow<Int> { throw RuntimeException("x") }.test {
assertTrue(awaitError() is RuntimeException)
}
}
}Coroutines Test (runTest)
runTest uses virtual time—delays skip instantly, making async tests fast and deterministic. Use StandardTestDispatcher + advanceUntilIdle() for manual control. Inject a TestDispatcher into production code (instead of Dispatchers.Main) for testability. Avoid runBlocking in tests—runTest is the modern way.
import kotlinx.coroutines.test.*
import kotlinx.coroutines.*
import kotlin.test.*
class RepoTest {
@Test fun fetch() = runTest {
// Virtual time: delay(1000) completes instantly
val result = async { delay(1000); "data" }.await()
assertEquals("data", result)
}
@Test fun withDispatcher() = runTest {
val scheduler = StandardTestDispatcher()
val scope = CoroutineScope(scheduler)
// advanceUntilIdle() runs pending coroutines
scope.launch { /*...*/ }
scheduler.advanceUntilIdle()
}
}Kotest Assertions & Styles
Kotest offers expressive assertions (shouldBe, shouldContain) and multiple spec styles (StringSpec, FunSpec, BehaviorSpec for BDD). It integrates with property-based testing and has rich matchers. Choose it over JUnit for Kotlin-idiomatic, readable tests. It can run alongside JUnit 5.
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith
class MyTest : StringSpec({
"addition works" {
1 + 1 shouldBe 2
}
"list contains" {
listOf(1, 2, 3) shouldContain 2
}
"string prefix" {
"hello" shouldStartWith "he"
}
})
// Other styles: FunSpec, BehaviorSpec, ShouldSpecFunctional Programming Deep Dive
Scope Functions (let, run, with, apply, also)
Scope functions differ by receiver (this vs it) and return value (this vs result). apply/also return the receiver (chaining, builders); let/run/with return the lambda result (transforms). Use apply for configuration, let for null-checks/transforms, also for side effects. Don't overuse—readability first.
data class User(var name: String, var age: Int)
fun main() {
val u = User("A", 1)
// apply: configure & return this (builders)
val configured = u.apply { age = 30 }
// let: transform, it = receiver, returns lambda result
val name = u.let { it.name.uppercase() }
// run: transform, this = receiver, returns result
val s = u.run { "$name:$age" }
// with: like run but called as function
val str = with(u) { "$name:$age" }
// also: side effects, returns this (logging/chaining)
u.also { println("Created $it") }
}Sequences (Lazy Evaluation)
Sequences evaluate lazily—each element flows through the whole pipeline before the next starts (like Java Streams). This avoids intermediate collections and short-circuits (take/find). Use for large datasets or multi-step pipelines. For small lists, eager List is often faster (less overhead). generateSequence builds infinite streams.
fun main() {
// Eager: List processes each step fully
val eager = (1..10).toList()
.map { it * 2 }
.filter { it > 5 }
// Lazy: Sequence processes element-by-element
val lazy = (1..10).asSequence()
.map { println("map $it"); it * 2 }
.filter { println("filter $it"); it > 5 }
.take(2)
.toList() // only processes until 2 found
// Use Sequence for large/pipelined data
// generateSequence for infinite streams
val naturals = generateSequence(1) { it + 1 }
}Inline Functions & reified
inline eliminates lambda object/allocation overhead by inlining bytecode—critical for hot loops. It also enables non-local returns (return from outer function inside a lambda). reified type parameters make generic type info available at runtime (T::class, is T), but require inline. Use for type-safe helpers like filterIsInstance.
// inline: copies bytecode at call site (no lambda overhead)
inline fun measure(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
// non-local return: inline lambdas can return from outer fun
inline fun forEach(list: List<Int>, f: (Int) -> Unit) {
for (i in list) f(i)
}
// reified: access generic type at runtime
inline fun <reified T> List<*>.filterIsInstance() =
filter { it is T } as List<T>
val strs = listOf(1, "a", 2, "b").filterIsInstance<String>()Higher-Order Functions
Higher-order functions take or return functions. They're the backbone of functional Kotlin—map, filter, fold, compose. Function types like (Int) -> Int are first-class. compose builds pipelines. Use them to abstract patterns, enable code reuse, and write declarative code. Mark inline for performance-critical paths.
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
val result = mutableListOf<R>()
for (item in this) result.add(transform(item))
return result
}
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int =
{ x -> f(g(x)) }
fun main() {
val double = { x: Int -> x * 2 }
val inc = { x: Int -> x + 1 }
val doubleThenInc = compose(inc, double)
println(doubleThenInc(3)) // 7
listOf(1, 2, 3).mapTo { it * it }
}Recursion & tailrec
tailrec converts tail-recursive functions (where recursion is the last operation) into loops, preventing stack overflow. The recursive call must be in tail position—no pending multiplication/addition. Use an accumulator parameter to make functions tail-recursive. Essential for functional-style loops on large inputs.
// Regular recursion: stack overflow on large n
fun factorial(n: Int): Long =
if (n <= 1) 1 else n * factorial(n - 1)
// tailrec: compiler optimizes to a loop (no stack growth)
tailrec fun factorialTail(n: Int, acc: Long = 1): Long =
if (n <= 1) acc else factorialTail(n - 1, acc * n)
// Fibonacci with tailrec
tailrec fun fib(n: Int, a: Long = 0, b: Long = 1): Long =
when (n) { 0 -> a; 1 -> b; else -> fib(n - 1, b, a + b) }
fun main() {
println(factorialTail(10000)) // no stack overflow
}Generics & Variance
Generic Classes & Functions
Generics enable type-safe, reusable code. Classes use <T>; functions declare <T> before the return type. Constraints (T : Entity) restrict the type bound. Unlike Java, Kotlin's generics are reified for inline functions and have declaration-site variance, making generic APIs safer and more ergonomic.
class Box<T>(val value: T) {
fun get(): T = value
}
fun <T> singletonList(item: T): List<T> = listOf(item)
fun main() {
val intBox = Box(42) // Box<Int> inferred
val strBox = Box<String>("hi")
val nums = singletonList(1)
}
// Generic constraints
class Repository<T : Entity> {
fun save(item: T) { /* T is Entity subtype */ }
}
interface Entity { val id: Int }Variance (in/out)
Variance controls subtype relationships of generics. out (covariant): a Producer<Dog> is a Producer<Animal>—safe because you only read T. in (contravariant): a Sink<Animal> is a Sink<Dog>—safe because you only write T. Mutable collections are invariant (read + write). Use out/in to design safe, flexible APIs.
// out (covariant): Producer<Sub> is Producer<Super>
interface Source<out T> { fun next(): T }
// in (contravariant): Consumer<Super> is Consumer<Sub>
interface Sink<in T> { fun put(item: T) }
// invariant (default): neither
class MutableList<T> {
fun add(item: T) {}
fun get(): T = TODO()
}
open class Animal
class Dog : Animal()
val src: Source<Animal> = Source<Dog>() // OK (out)
val sink: Sink<Dog> = Sink<Animal>() // OK (in)Variance in Practice
Function types use variance automatically: parameters are 'in', return types are 'out'. This is why (Dog) -> Unit is assignable to (Animal) -> Unit. The PECS principle (Producer Extends, Consumer Super) maps to Kotlin's out/in. Design generic interfaces with out when they only produce T, in when they only consume T.
// Function types are variant by nature
// (T) -> R is contravariant in T, covariant in R
val dogHandler: (Dog) -> Unit = { println(it) }
val animalHandler: (Animal) -> Unit = dogHandler // OK
// Covariant return types
interface Repository<out T> { fun find(id: Int): T }
class DogRepo : Repository<Dog> {
override fun find(id: Int): Dog = Dog()
}
val repo: Repository<Animal> = DogRepo() // OK
// PECS rule: Producer extends, Consumer super
// Kotlin: out = producer, in = consumerType Projections & Star Projection
Type projections temporarily make a type variant at a use-site. Array<out Any> means 'Array of some subtype of Any, readable'. Array<in Any> means 'writable as Any'. Star projection (*) means 'some unknown type'—useful when you only need size/contains, not the element type. Like Java's wildcards.
fun copy(from: Array<out Any>, to: Array<in Any>) {
// from: only read (covariant projection)
// to: only write (contravariant projection)
for (i in from.indices) to[i] = from[i]
}
// Star projection: unknown type, read-only
fun printSize(list: List<*>) {
println(list.size) // OK (no type needed)
// list[0] // type is Any? — limited use
}
val mixed: List<*> = listOf(1, "a", 3.0)
// Use when type is irrelevant or unknownReified Type Parameters
reified preserves generic type info at runtime (normally erased on JVM). It requires inline (the type is known at each call site). Enables is T checks, T::class, and filterIsInstance. Without reified, you'd pass a Class<T> parameter manually. Limitation: reified types can't be used in non-inline functions or as class type parameters.
// reified requires inline
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
filter { it is T } as List<T>
inline fun <reified T> Any.castTo(): T = this as T
inline fun <reified T> typeOf() = T::class.simpleName
fun main() {
val list = listOf(1, "a", 2, "b", 3)
val strs: List<String> = list.filterIsInstance()
println(strs) // [a, b]
println(typeOf<String>()) // String
// Without reified, T is erased at runtime
// reified makes T::class and 'is T' available
}Null Safety Deep
Nullable Types
Kotlin distinguishes nullable (T?) and non-null (T) types at compile time. ?. safe call returns null if receiver is null. ?: Elvis provides a default. !! throws NPE (avoid). The compiler enforces null checks, eliminating NullPointerException in Kotlin code.
var name: String = "Alice" // Non-null
// name = null // Error
var nickname: String? = null // Nullable
// Safe call
println(nickname?.length) // null if nickname is null
// Elvis operator
val len = nickname?.length ?: 0 // 0 if null
// Not-null assertion (use sparingly)
val len2 = nickname!!.length // NPE if nulllet & run
let executes a block if the value is non-null (it = the value). run calls a block with the object as receiver, returns result. apply configures an object, returns the object. also performs side effects, returns the object. These scope functions reduce null checks and improve readability.
val name: String? = "Alice"
name?.let {
println("Length: ${it.length}") // Only if not null
}
// run: object + block
val result = "Hello".run {
length // Returns 5
}
// apply: configure object
val list = mutableListOf<Int>().apply {
add(1); add(2); add(3)
}lateinit
lateinit defers initialization of non-null properties. Cannot be used with primitives or nullable types. Throws UninitializedPropertyAccessException if accessed before init. ::prop.isInitialized checks. Useful for dependency injection and lifecycle-managed properties.
class Service {
lateinit var dependency: Database
fun init() {
dependency = connectDatabase()
}
fun use() {
if (::dependency.isInitialized) {
dependency.query()
}
}
}Nullable Collections
Collections can hold nullable elements (List<String?>) or be nullable (List<String>?). filterNotNull removes nulls. firstOrNull returns null instead of throwing. Handle nullable elements with safe calls. Be explicit about nullability in collection types.
val list: List<String?> = listOf("a", null, "b")
val filtered = list.filterNotNull() // ["a", "b"]
val lengths = list.map { it?.length } // [1, null, 1]
val first = list.firstOrNull() // "a" or null
val firstNonEmpty = list.firstOrNull { it?.isNotEmpty() == true }Platform Types
Platform types arise from Java interop where nullability is unknown. Kotlin cannot enforce null safety for them. Always declare nullable types explicitly for Java return values. Use @Nullable/@NotNull annotations in Java. JSR-305 annotations help Kotlin infer nullability.
// Java interop: platform type (no null info)
val name: String = javaObject.getName() // May be null!
// Kotlin does not know if Java returns null
// Fix: explicit nullable type
val name2: String? = javaObject.getName()
// Or @Nullable annotation in JavaDSL Construction
Builder with Lambda Receiver
Lambda with receiver (T.() -> Unit) enables DSL syntax. Inside the lambda, this is the receiver object. Methods can be called without qualification. This is how Kotlin builds type-safe DSLs like Gradle, HTML, and SQL builders.
class StringBuilder {
private val parts = mutableListOf<String>()
fun line(s: String) { parts.add(s) }
fun build() = parts.joinToString("\n")
}
fun buildString(init: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.init() // Execute lambda with sb as receiver
return sb.build()
}HTML DSL
HTML DSL uses nested builders with lambda receivers. Each tag is a function that creates a child builder. The lambda configures the child. Produces type-safe, composable HTML. kotlinx.html is a real implementation. Same pattern works for any hierarchical structure.
fun html(init: HTML.() -> Unit): HTML {
val h = HTML(); h.init(); return h
}
class HTML {
fun body(init: Body.() -> Unit) { /* ... */ }
}
html {
body {
// this: Body
p("Hello")
}
}@DslMarker
@DslMarker prevents implicit receiver access to outer scopes. Without it, both HTML and Body methods are accessible, causing confusion. The annotation restricts access to the innermost receiver. Makes DSLs type-safe and unambiguous. Essential for complex DSLs.
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(...) {} }
@HtmlDsl
class Body { fun p(...) {} }
html {
body {
// p() // OK: in Body scope
// body() // Error: in Body scope, not HTML
}
}Gradle DSL
Gradle Kotlin DSL uses the same builder pattern. plugins, dependencies are functions with receiver lambdas. implementation, testImplementation are dependency configuration functions. Type-safe: compiler checks function names and parameter types. Much better than Groovy for refactoring.
plugins {
kotlin("jvm") version "1.9.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation(kotlin("test"))
}
// All are function calls with lambda receiversAnko SQL
Anko (and Exposed) provide type-safe SQL DSLs. Column comparisons are typed. Cannot compare a string column to an int. The DSL generates SQL. Prevents SQL injection and type errors. Same pattern applies to any domain-specific language.
fun users(where: SqlExpressionBuilder.() -> Op<Boolean>) {
// DSL for SQL queries
}
users {
(Users.age greater 18) and (Users.name like "A%")
}
// Type-safe SQL: compiler checks column typesCollections
List Operations
Kotlin collections have rich functional APIs. map, filter, reduce are standard. groupBy partitions by key. chunked splits into fixed-size lists. windowed creates sliding windows. All return new collections. Use asSequence() for lazy evaluation.
val list = listOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
val grouped = list.groupBy { it % 2 } // {0=[2,4], 1=[1,3,5]}
val chunked = list.chunked(2) // [[1,2],[3,4],[5]]Sequence
Sequence is a lazy collection (like Java Stream). Operations are evaluated on demand. No intermediate collections. Efficient for large collections with chained operations. Use asSequence() to convert. Force with toList(), toSet(), etc.
val result = (1..1000000).asSequence()
.map { it * 2 }
.filter { it > 100 }
.take(10)
.toList()
// Lazy: only computes what is needed
// No intermediate collectionsDestructuring
Destructuring splits objects into variables. Works with Pair, Triple, data classes, and Map entries. componentN() functions enable it. withIndex() pairs index and value. Useful for multiple return values and iteration. Data classes auto-generate componentN.
val (a, b) = Pair(1, "hello")
val (x, y, z) = Triple(1, 2, 3)
for ((index, value) in list.withIndex()) {
println("$index: $value")
}
data class Point(val x: Int, val y: Int)
val (px, py) = Point(1, 2)Mutable vs Immutable
Kotlin has both mutable (MutableList) and immutable (List) collections. Prefer immutable for safety. toList() creates an immutable copy. toMutableList() creates a mutable copy. The immutable interfaces do not expose mutation methods, preventing accidental modification.
val mutable = mutableListOf(1, 2, 3)
mutable.add(4) // OK
val immutable = listOf(1, 2, 3)
// immutable.add(4) // Error: no add method
// Convert
val imm = mutable.toList()
val mut = imm.toMutableList()Associate & Partition
toMap/associate convert to maps. partition splits into two lists by predicate. flatten merges nested lists. flatMap maps and flattens. These replace verbose loops with declarative expressions. All return new collections.
val list = listOf("a" to 1, "b" to 2)
val map = list.toMap() // {a=1, b=2}
val byLength = list.associate { it.first to it.second }
val (evens, odds) = list.partition { it.second % 2 == 0 }
// evens: [(a,1)?] odds: [(b,2)]
val flat = listOf(listOf(1), listOf(2, 3)).flatten() // [1,2,3]Common Pitfalls
== vs ===
Kotlin == calls equals (value equality), unlike Java. === checks reference equality. Use == for value comparison. === is rarely needed. For Integer, values -128 to 127 are cached, so === may be true or false. Always use == for values.
val a = Integer(127)
val b = Integer(127)
a == b // true (value equality, calls equals)
a === b // true (Integer caches -128 to 127)
val c = Integer(128)
val d = Integer(128)
c == d // true
c === d // false (not cached)Companion Object
Kotlin does not have static members. companion object holds "static" methods and constants. const val is a true compile-time constant. The companion object is a singleton instance. @JvmStatic makes methods callable as static from Java. Use top-level functions for true statics.
class MyClass {
companion object {
const val CONSTANT = 42
fun create() = MyClass()
}
}
MyClass.CONSTANT // 42
MyClass.create() // Static-like access
// companion object is a real object, not staticData Class Copy
Data classes auto-generate copy() which creates a modified copy. Original is unchanged (immutable). Only specified fields change. Useful for updates. Combined with destructuring, data classes are powerful for modeling data. Avoid var in data classes for immutability.
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31) // New instance
// alice is unchanged
// copy uses named arguments for changed fieldsSealed Class Exhaustiveness
Sealed classes restrict subtypes to the same file/package. when on sealed classes is exhaustive: the compiler warns if a branch is missing. No else needed if all cases are covered. Adding a new subtype causes warnings everywhere. Ideal for state machines and results.
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
fun handle(r: Result) = when (r) {
is Success -> r.value
// Warning: Missing Failure branch
}
// Fix: handle all cases
fun handle(r: Result) = when (r) {
is Success -> r.value
is Failure -> 0
} // No else neededExtension vs Member
Member functions always take precedence over extension functions with the same signature. Extensions are resolved at compile time (static dispatch), members at runtime (dynamic dispatch). Extensions cannot override members. Use extensions for utility functions, not for polymorphism.
class Foo {
fun bar() = "member" // Member function
}
fun Foo.bar() = "extension" // Extension function
Foo().bar() // "member" - members win!
// Extensions are resolved statically
// Members are resolved dynamically (virtual)Kotlin Multiplatform
Common Code
Kotlin Multiplatform (KMP) shares code across platforms. expect/actual declares platform-specific implementations. commonMain has shared code. Platform-specific source sets implement actual. Share business logic, keep UI native. Gradle configures targets.
// commonMain/src/Platform.kt
expect fun getPlatformName(): String
// androidMain/src/Platform.kt
actual fun getPlatformName(): String = "Android"
// iosMain/src/Platform.kt
actual fun getPlatformName(): String = "iOS"
// Shared business logic
class Greeting {
fun greet() = "Hello from ${getPlatformName()}"
}Shared Module
Multiplatform projects use kotlin block in Gradle. Define targets (android, ios). commonMain has shared dependencies. Platform source sets can have platform-specific deps. iOS uses Kotlin/Native for direct compilation. Share logic, not UI.
// build.gradle.kts (shared module)
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }
}
}
}Networking (Ktor)
Ktor is a multiplatform HTTP client. The API is common, the engine is platform-specific. Android uses OkHttp/Android engine. iOS uses Darwin. Coroutines work across platforms. Share networking, database (SQLDelight), and business logic.
// Shared HTTP client
suspend fun fetchUser(): User {
return client.get("https://api.example.com/user").body()
}
// Platform-specific engine
// Android: OkHttp or Android
// iOS: Darwin
// Common code uses the same APISQLDelight
SQLDelight generates type-safe Kotlin from SQL. .sq files contain SQL with named queries. Generates type-safe query objects. Works across platforms (Android, iOS, JVM). SQL is the source of truth. Schema migrations are tracked. Alternative to Room for multiplatform.
// .sq file: User.sq
CREATE TABLE User (id INTEGER, name TEXT);
selectById: SELECT * FROM User WHERE id = ?;
insertUser: INSERT INTO User (id, name) VALUES (?, ?);
// Generated Kotlin code
val queries: UserQueries = database.userQueries
queries.insertUser(1, "Alice")
val user = queries.selectById(1).executeAsOne()Compose Multiplatform
Compose Multiplatform extends Jetpack Compose to iOS, Desktop, and Web. Share UI code across platforms. Same @Composable API. Platform-specific entry points. Still experimental for iOS. Reduces UI duplication. Gradle plugin: compose-multiplatform.
// Shared UI with Compose Multiplatform
@Composable
fun Greeting(name: String) {
Text("Hello, $name!")
}
// Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { Greeting("World") }
}
}
// Desktop
fun main() = application {
Window { Greeting("Desktop") }
}Testing Kotlin
JUnit 5
JUnit 5 is the standard testing framework. Kotlin allows backtick test names for readability. assertEquals, assertThrows are common assertions. @BeforeEach, @AfterEach for setup/teardown. @ParameterizedTest for data-driven tests. Use kotlin.test for multiplatform.
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
@Test
fun `test addition`() {
val calc = Calculator()
assertEquals(5, calc.add(2, 3))
}
@Test
fun `throws on negative`() {
assertThrows<IllegalArgumentException> { calc.sqrt(-1) }
}
}MockK
MockK is a Kotlin-native mocking library. Supports final classes and extension functions. every stubs, verify checks calls. @MockK creates mocks, @InjectMockKs injects them. coEvery/coVerify for coroutines. Better Kotlin support than Mockito.
class UserServiceTest {
@MockK
lateinit var repo: UserRepository
@InjectMockKs
lateinit var service: UserService
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { repo.find(1) } returns User("Alice")
}
@Test
fun `test find`() {
assertEquals("Alice", service.find(1).name)
verify { repo.find(1) }
}
}Kotest
Kotest is a Kotlin-first testing framework. Multiple styles: StringSpec, BehaviorSpec, FunSpec. shouldBe is a fluent assertion. Property testing with Arb. Supports data-driven tests. Integrates with Spring and Ktor. More Kotlin-idiomatic than JUnit.
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
"addition should work" {
Calculator().add(2, 3) shouldBe 5
}
"should throw on negative" {
shouldThrow<IllegalArgumentException> { Calculator().sqrt(-1) }
}
})Coroutine Testing
runTest from kotlinx-coroutines-test provides virtual time. Delays skip instantly. advanceUntilIdle runs pending coroutines. Much faster than real time. Use TestDispatcher for fine control. Turbine library tests Flow emissions.
@Test
fun `test async`() = runTest {
val result = fetchData()
assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantlyTurbine (Flow Testing)
Turbine tests Flow emissions. awaitItem gets next emission. awaitComplete/asserts completion. awaitError asserts an error. test block suspends until the flow completes. Much cleaner than collecting manually. Essential for Flow testing.
@Test
fun `test flow`() = runTest {
flowOf(1, 2, 3).test {
awaitItem() shouldBe 1
awaitItem() shouldBe 2
awaitItem() shouldBe 3
awaitComplete()
}
}
// Test errors
flow<Int> { throw Exception() }.test {
awaitError() shouldBe Exception()
}Coroutines Deep Dive
Coroutine Scope
CoroutineScope defines a lifetime for coroutines. viewModelScope auto-cancels on VM clear. SupervisorJob prevents child failures from cancelling siblings. Custom scopes need explicit cancellation. Scopes propagate cancellation to children. Never use GlobalScope in production (cannot be cancelled).
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
val data = api.getData()
_data.value = data
}
}
}
// Custom scope
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { /* ... */ }
scope.cancel() // Cancel all childrenDispatchers
Dispatchers route coroutines to thread pools. Main: UI thread (Android). IO: blocking I/O (64+ threads). Default: CPU work (CPU count threads). Unconfined: caller thread (advanced). withContext switches dispatcher temporarily. Choosing the right dispatcher improves performance and prevents UI freezes.
launch(Dispatchers.Main) { updateUI() }
launch(Dispatchers.IO) { writeFile() }
launch(Dispatchers.Default) { heavyComputation() }
launch(Dispatchers.Unconfined) { runAnywhere() }
// Switch context
withContext(Dispatchers.IO) {
val data = readDisk()
}Flow Operators
Flow operators: map transforms, filter selects, flatMapMerge/flatMapConcat chain flows. Cold flows start fresh per collector. StateFlow holds a value (like LiveData). SharedFlow broadcasts to multiple collectors. stateIn converts cold to hot. buffer/conflate control backpressure.
flowOf(1, 2, 3, 4, 5)
.map { it * 2 }
.filter { it > 4 }
.collect { println(it) } // 6, 8, 10
// Cold flow: emits per collector
// StateFlow: hot, stateful
// SharedFlow: hot, broadcast
val state = MutableStateFlow(0)
state.value = 1Exception Handling
CoroutineExceptionHandler catches uncaught exceptions in launch. async exceptions propagate to await. SupervisorJob isolates child failures. CancellationException is special: rethrown, not caught by catch. Never swallow CancellationException. Use try/finally or use() for cleanup. Cancellation propagates through suspend calls.
val handler = CoroutineExceptionHandler { _, e ->
Log.e("TAG", "Caught: $e", e)
}
scope.launch(handler) {
throw RuntimeException("oops")
}
// try/catch in coroutine
try {
riskyCall()
} catch (e: Exception) {
// handle
}Channels
Channels allow coroutine-to-coroutine communication. send suspends when full, receive suspends when empty. capacity: buffered (RENDEZVOUS=0, UNLIMITED, CONFLATED). close() signals completion. produce creates a producer coroutine. Channel is hot: values are consumed once. Prefer Flow for most use cases.
val channel = Channel<Int>(capacity = 10)
launch {
for (i in 1..5) channel.send(i)
channel.close()
}
launch {
for (x in channel) println(x)
}
// Produce pattern
fun numbers() = produce {
for (i in 1..10) send(i)
}Related Kotlin snippets
Copy-paste ready code for common tasks.
Null Safety
Use nullable types, safe calls, Elvis, and smart casts for null-safe code.
Data Classes
Model immutable data with auto-generated equals, copy, and destructuring.
Coroutines
Use launch, async, await, and structured concurrency with supervisorScope.
Extension Functions
Add methods to existing types with extensions and infix operators.
Sealed Classes
Model closed hierarchies and UI state with sealed classes and when.
When Expression
Branch on values, ranges, and types with when as statement or expression.
Collections
Filter, map, group, partition, and chunk with functional operators.
Delegation
Delegate interfaces, lazy properties, observables, and custom delegates.
Was this helpful?