Collections — Lists, Sets, Maps & Arrays

Read-only interfaces over mutable JVM collections — which to pick and what each operation costs.

  • jvm
  • android
  • kotlin >=2.0
  • jvm >=17
  • Kotlin/JVM (the standard library)

Sources: Collections overview — Kotlin docs

Pick the container

The defaults keep insertion order — `mutableSetOf` and `mutableMapOf` are linked hash collections.

NeedUseWhy
ordered listList<T> / mutableListOf()an ArrayList underneath; List is a read-only view
membership, dedupeSet<T> / mutableSetOf()LinkedHashSet: O(1) contains, insertion order
key → valueMap<K, V> / mutableMapOf()LinkedHashMap; hashMapOf() drops the order for a little speed
sorted keyssortedMapOf() / java.util.TreeMapO(log n), range queries
queue or stackArrayDeque<T>O(1) at both ends
priority queuejava.util.PriorityQueueO(log n) offer and poll
primitives in bulkIntArray, DoubleArray…int[] underneath — no boxing

What operations cost

The JVM implementations behind the Kotlin interfaces.

TypeLookupInsertRemove
ArrayListO(1) by index, O(n) by valueO(1)* at the end, O(n) elsewhereO(1) last, O(n) other
LinkedHashSet / LinkedHashMapO(1) averageO(1) averageO(1) average
TreeMap / TreeSetO(log n)O(log n)O(log n)
ArrayDequeO(1) by indexO(1)* at either endO(1) at either end
PriorityQueueO(1) peekO(log n)O(log n) poll, O(n) by value

Read-only is not immutable

`List` hides the mutators; the object underneath can still change through another reference.

fun main() {
    val readOnly: List<Int> = listOf(3, 1, 2)
    val mutable = readOnly.toMutableList()      // a copy you may change
    mutable += 4
    println("$readOnly $mutable")

    val view: List<Int> = mutable               // a read-only VIEW of the same list
    mutable.clear()
    println(view.size)
}
Result
[3, 1, 2] [3, 1, 2, 4]
0
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: List, Set and Map — Kotlin docs

Maps and sets

`getValue` throws on a missing key; `[]` returns null; set algebra reads as infix words.

fun main() {
    val counts = mutableMapOf<String, Int>()
    for (word in "the cat and the hat".split(" ")) {
        counts[word] = counts.getOrDefault(word, 0) + 1
    }
    println(counts)
    println(counts.getValue("the") to ("dog" in counts))

    val a = setOf(1, 2, 3)
    val b = setOf(2, 3, 4)
    println("${a union b} ${a intersect b} ${a subtract b}")
}
Result
{the=2, cat=1, and=1, hat=1}
(2, false)
[1, 2, 3, 4] [2, 3] [1]
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Keys need value equality

A `data class` generates `equals` and `hashCode`; a plain class compares by identity.

data class Point(val x: Int, val y: Int)
class Cell(val x: Int, val y: Int)

fun main() {
    println(setOf(Point(1, 1), Point(1, 1)).size)
    println(setOf(Cell(1, 1), Cell(1, 1)).size)
}
Result
1
2
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Data classes — Kotlin docs

ArrayDeque and primitive arrays

`ArrayDeque` is the queue and the stack; `IntArray` is `int[]` with no boxing.

fun main() {
    val queue = ArrayDeque<String>()
    queue.addLast("a")
    queue.addLast("b")
    queue.addFirst("urgent")
    println("${queue.removeFirst()} ${queue.removeLast()} $queue")

    val primes = intArrayOf(2, 3, 5, 7)
    println(primes.sum())
}
Result
urgent b [a]
17
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Pitfalls

Where Kotlin differs from Swift's value-type collections.

  • Collections are references: val ys = xs aliases a MutableList. Copy with toList() / toMutableList().
  • list.removeAt(0) in a loop is O(n²) — use ArrayDeque.
  • Mutating a collection while iterating it throws ConcurrentModificationException; use removeAll { } or iterate a copy.
  • Expose List, Set, Map from APIs; keep the Mutable* type private.
  • List<Int> boxes every element; IntArray does not — it matters for large numeric data.

Collections — Operation Cookbook

Transform, fold, group, sort, chunk, window and zip — eager on collections, lazy on sequences.

  • jvm
  • android
  • kotlin >=2.0
  • jvm >=17
  • Kotlin/JVM (the standard library)

Sources: Collection operations overview — Kotlin docs

I want to…

Each returns a new collection; the `Mutable*` variants (`sort`, `removeAll`…) change in place.

GoalFunction
transform eachmap, mapNotNull, flatMap
keep somefilter, filterNot, filterIsInstance<T>()
fold to one valuesumOf, fold, reduce
first / last matchfirst { }, firstOrNull { }, lastOrNull { }
testany, all, none, in
sortsorted(), sortedBy { }, sortedWith(compareBy(...).thenBy(...))
group / index / countgroupBy, associateBy, groupingBy { }.eachCount()
split in twopartition { }
slicetake, drop, chunked, windowed
pair upzip, zipWithNext
dedupedistinct(), distinctBy { }

Transform and fold

`sumOf` says the intent; `fold` is the general form.

data class Order(val id: Int, val customer: String, val total: Int)

fun main() {
    val orders = listOf(Order(1, "ana", 30), Order(2, "bo", 12), Order(3, "ana", 8))
    println(orders.filter { it.total >= 10 }.map { it.id })
    println(orders.sumOf { it.total })
    println(orders.fold(0) { acc, o -> acc + o.total })
}
Result
[1, 2]
50
50
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Group, index, count, partition

Groups keep the order in which keys first appear.

data class Person(val name: String, val team: String)

fun main() {
    val people = listOf(Person("Ana", "web"), Person("Bo", "data"), Person("Cy", "web"))
    println(people.groupBy({ it.team }, { it.name }))
    println(people.associateBy { it.name }.getValue("Bo").team)
    println(people.groupingBy { it.team }.eachCount())
    val (web, others) = people.partition { it.team == "web" }
    println("${web.size} ${others.size}")
}
Result
{web=[Ana, Cy], data=[Bo]}
data
{web=2, data=1}
2 1
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Grouping — Kotlin docs

Sort by several keys

Comparators compose: `compareByDescending { }.thenBy { }`. Sorting is stable.

data class Player(val name: String, val score: Int)

fun main() {
    val players = listOf(Player("cy", 7), Player("ana", 9), Player("bo", 7))
    val ranked = players.sortedWith(compareByDescending<Player> { it.score }.thenBy { it.name })
    println(ranked.joinToString(" ") { "${it.name}:${it.score}" })
    println("${players.maxBy { it.score }.name} ${players.minByOrNull { it.score }?.name}")
}
Result
ana:9 bo:7 cy:7
ana cy
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Ordering — Kotlin docs

Chunk, window, dedupe, zip

All built in.

fun main() {
    println((1..5).chunked(2))
    println((1..4).windowed(3))
    println(listOf("b", "a", "b", "c", "a").distinct())
    println(listOf("a", "b", "c").zip(listOf(1, 2)))     // stops at the shorter
    println(listOf(1, 2, 4, 7).zipWithNext { a, b -> b - a })
}
Result
[[1, 2], [3, 4], [5]]
[[1, 2, 3], [2, 3, 4]]
[b, a, c]
[(a, 1), (b, 2)]
[1, 2, 3]
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Search and test

`first { }` throws when nothing matches; the `OrNull` variants return null.

fun main() {
    val xs = listOf(3, 8, 12, 5, 20)
    println("${xs.first { it > 10 }} ${xs.lastOrNull { it > 10 }} ${xs.indexOfFirst { it > 100 }} ${xs.firstOrNull { it > 100 }}")
    println("${xs.any { it % 2 == 0 }} ${xs.all { it > 0 }} ${5 in xs} ${xs.none { it < 0 }}")
}
Result
12 20 -1 null
true true true true
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Lazy pipelines with sequences

A sequence runs each element through the whole chain and stops as soon as `take` is satisfied.

fun main() {
    var squared = 0
    val firstOddSquares = generateSequence(1) { it + 1 }
        .map { squared++; it * it }
        .filter { it % 2 == 1 }
        .take(3)
        .toList()
    println("$firstOddSquares after $squared squares")
}
Result
[1, 9, 25] after 5 squares
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Sequences — Kotlin docs

Coroutines — Core

suspend functions, structured concurrency, async/await, failure propagation, cancellation and timeouts.

  • jvm
  • android
  • kotlinx-coroutines-core
  • kotlin >=2.0
  • kotlinx-coroutines >=1.10
  • jvm >=17
  • Kotlin/JVM with kotlinx.coroutines

Sources: Coroutines basics — Kotlin docs · Cancellation and timeouts — Kotlin docs

Sequential vs concurrent

Calls are sequential by default; `async` makes concurrency explicit.

import kotlinx.coroutines.*
import kotlin.time.measureTime

suspend fun load(name: String): String {
    delay(100)                             // suspends; the thread is free meanwhile
    return name
}

fun main() = runBlocking {
    val sequential = measureTime { load("user"); load("posts") }
    val concurrent = measureTime {
        val user = async { load("user") }
        val posts = async { load("posts") }
        println("${user.await()} ${posts.await()}")
    }
    // about 200 ms against about 100 ms; compared, not printed, so a busy machine cannot change the output
    println("concurrent took well under the sequential time: ${concurrent < sequential * 0.75}")
}
Result
user posts
concurrent took well under the sequential time: true
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Structured fan-out

`coroutineScope` waits for every child and fails if one fails — the TaskGroup of Kotlin.

import kotlinx.coroutines.*

suspend fun thumbnail(id: Int): String {
    delay(10L * (4 - id))                  // later ids finish first
    return "thumb-$id"
}

suspend fun thumbnails(ids: List<Int>): Map<Int, String> = coroutineScope {
    ids.map { id -> async { id to thumbnail(id) } }   // all start now
        .awaitAll()                                   // results in INPUT order
        .toMap()
}

fun main() = runBlocking {
    println(thumbnails(listOf(1, 2, 3)))
}
Result
{1=thumb-1, 2=thumb-2, 3=thumb-3}
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Sources: Composing suspending functions — Kotlin docs

Failure propagation and supervision

A failing child cancels its siblings in `coroutineScope`; in `supervisorScope` each child fails alone.

import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        coroutineScope {
            launch { delay(1_000); println("never printed: cancelled by its sibling") }
            launch { delay(10); error("boom") }
        }
    } catch (e: IllegalStateException) {
        println("caught ${e.message}")
    }

    val results = supervisorScope {
        val a = async { delay(10); 1 }
        val b = async<Int> { throw IllegalArgumentException("bad input") }
        listOf(runCatching { a.await() }, runCatching { b.await() })
    }
    println(results.map { r -> r.getOrElse { it.message } })
}
Result
caught boom
[1, bad input]
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Sources: Coroutine exceptions handling — Kotlin docs

Cancellation and timeouts

Cancellation is cooperative: every suspension point checks it, and `finally` runs like Swift's `defer`.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val ticks = Channel<Int>()
    val job = launch {
        try {
            repeat(10) { i ->
                ticks.send(i)
                delay(100)                     // a suspension point: where cancellation lands
            }
        } finally {
            println("cleanup")
        }
    }
    repeat(3) { println("tick ${ticks.receive()}") }
    job.cancelAndJoin()                        // e.g. the screen went away after three ticks

    val result = withTimeoutOrNull(150) { delay(1_000); "done" }
    println("timeout -> $result")
}
Result
tick 0
tick 1
tick 2
cleanup
timeout -> null
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

CPU work off the caller's thread

`withContext(Dispatchers.Default)` moves the work; a loop without suspension must call `ensureActive()` to stay cancellable.

import kotlinx.coroutines.*

suspend fun countPrimes(limit: Int): Int = withContext(Dispatchers.Default) {
    var count = 0
    for (n in 2..limit) {
        ensureActive()
        if ((2..Math.sqrt(n.toDouble()).toInt()).none { n % it == 0 }) count++
    }
    count
}

fun main() = runBlocking {
    println(countPrimes(10_000))
}
Result
1229
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Bridging callbacks and futures

`suspendCancellableCoroutine` is `withCheckedContinuation`; `CompletableFuture.await()` ships in coroutines-core.

import kotlinx.coroutines.*
import kotlinx.coroutines.future.await
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.resume

fun fetchTheme(callback: (String) -> Unit) {          // a callback API you do not own
    Thread { Thread.sleep(20); callback("dark") }.start()
}

suspend fun fetchTheme(): String = suspendCancellableCoroutine { cont ->
    fetchTheme { value -> cont.resume(value) }
}

fun main() = runBlocking {
    println(fetchTheme())
    println(CompletableFuture.supplyAsync { 6 * 7 }.await())
}
Result
dark
42
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Swift concurrency → coroutines

The concepts map one to one; the defaults differ.

SwiftKotlinDifference
async letasync { } + await()needs a scope; async is a builder, not a keyword
withThrowingTaskGroupcoroutineScope { map { async { } }.awaitAll() }same structured semantics
Task { }scope.launch { }always launched in an explicit scope
Task.detachedGlobalScope.launchdiscouraged for the same reasons
Task.checkCancellation()ensureActive()CancellationException must be rethrown, never swallowed
withCheckedContinuationsuspendCancellableCoroutineresuming twice throws IllegalStateException
@MainActorDispatchers.Main / withContext(Dispatchers.Main)a dispatcher, not a compiler-checked isolation domain

Rules

What reviewers flag first.

  • runBlocking inside a coroutine or on the Android main thread blocks the thread it runs on.
  • catch (e: Exception) around a suspend call swallows CancellationException — rethrow it, or catch narrower types.
  • Launch in a scope with a lifetime (viewModelScope, lifecycleScope, your own CoroutineScope), never GlobalScope.
  • Make suspend functions main-safe: they switch dispatcher themselves with withContext.

Algorithmic Patterns & Complexity

The JVM collections' costs, then the patterns that solve most problems — with a heap and a deque built in.

  • jvm
  • android
  • kotlin >=2.0
  • jvm >=17
  • Kotlin/JVM (the standard library and java.util)

Sources: Collections overview — Kotlin docs · PriorityQueue — Java SE 17 API

Built-in costs

What the standard library hands you, and what to reach for instead.

NeedBuilt-inCost
stack / queueArrayDequeO(1) at both ends
priority queuejava.util.PriorityQueueO(log n) offer / poll, O(1) peek
sorted map / setTreeMap, TreeSetO(log n); floorKey, ceiling, ranges
sortsorted() / sortWithO(n log n); stable for objects (TimSort)
binary searchList.binarySearchO(log n); returns -(insertion point) - 1 on a miss
build a stringbuildString { }+= in a loop is O(n²)
big integersjava.math.BigIntegerInt overflows silently

Two pointers

On sorted input, move the pointer that brings the sum closer. O(n), O(1) space.

fun pairWithSum(sorted: List<Int>, target: Int): Pair<Int, Int>? {
    var lo = 0
    var hi = sorted.lastIndex
    while (lo < hi) {
        val sum = sorted[lo] + sorted[hi]
        when {
            sum == target -> return lo to hi
            sum < target -> lo++
            else -> hi--
        }
    }
    return null
}

fun main() = println("${pairWithSum(listOf(1, 3, 4, 6, 9), 10)} ${pairWithSum(listOf(1, 2), 7)}")
Result
(0, 4) null
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sliding window

Longest substring without repeats — grow the right edge, jump the left past the last repeat. O(n).

fun longestUnique(s: String): Int {
    val lastSeen = HashMap<Char, Int>()
    var start = 0
    var best = 0
    for ((i, ch) in s.withIndex()) {
        val prev = lastSeen[ch]
        if (prev != null && prev >= start) start = prev + 1
        lastSeen[ch] = i
        best = maxOf(best, i - start + 1)
    }
    return best
}

fun main() = println("${longestUnique("abcabcbb")} ${longestUnique("pwwkew")}")
Result
3 3
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Heap — top k

A min-heap of size k keeps the k largest; its root is the smallest of them. O(n log k).

import java.util.PriorityQueue

fun topK(xs: List<Int>, k: Int): List<Int> {
    val heap = PriorityQueue<Int>()
    for (x in xs) {
        heap.offer(x)
        if (heap.size > k) heap.poll()
    }
    return heap.sortedDescending()
}

fun main() = println(topK(listOf(5, 1, 9, 3, 7, 9, 2), 3))
Result
[9, 9, 7]
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Breadth-first search on a grid

BFS finds shortest paths in unweighted graphs; `ArrayDeque` makes each dequeue O(1).

fun shortestPath(grid: List<String>, from: Pair<Int, Int>, to: Pair<Int, Int>): Int {
    val dist = hashMapOf(from to 0)
    val queue = ArrayDeque(listOf(from))
    while (queue.isNotEmpty()) {
        val cell = queue.removeFirst()
        val here = dist.getValue(cell)
        if (cell == to) return here
        val (r, c) = cell
        for ((dr, dc) in listOf(1 to 0, -1 to 0, 0 to 1, 0 to -1)) {
            val next = r + dr to c + dc
            val (nr, nc) = next
            if (nr !in grid.indices || nc !in grid[nr].indices || grid[nr][nc] == '#' || next in dist) continue
            dist[next] = here + 1
            queue.addLast(next)
        }
    }
    return -1
}

fun main() = println(shortestPath(listOf("..#.", ".##.", "...."), 0 to 0, 0 to 3))
Result
7
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Working a problem out loud

The habits that matter more than the language.

  • State the brute force and its cost first, then the bottleneck the better pattern removes.
  • Name the edge cases before coding: empty input, one element, duplicates, Int overflow.
  • (lo + hi) / 2 can overflow on huge indices — ushr 1 or lo + (hi - lo) / 2.
  • List<Int> boxes; for tight numeric loops prefer IntArray.

Testing Approaches in Kotlin

JUnit 5, virtual time with kotlinx-coroutines-test, and seams that make tests fast and deterministic.

  • jvm
  • android
  • kotlin >=2.0
  • jvm >=17
  • Kotlin/JVM with a test runner

Sources: Test code using JUnit in JVM — Kotlin docs · kotlinx-coroutines-test — API reference

Tooling matrix

The common JVM and Android choices.

NeedTool
runner and assertionsJUnit 5 (org.junit.jupiter) or kotlin.test
coroutines and virtual timekotlinx-coroutines-test: runTest, advanceTimeBy, StandardTestDispatcher
Flow assertionsTurbine (app.cash.turbine)
test doubleshand-written fakes; MockK when a fake is impractical
Android without a deviceRobolectric; Compose createComposeRule()
coverageKover or JaCoCo

Virtual time with runTest

Inside `runTest`, `delay` advances a virtual clock — a 300 ms debounce is tested instantly.

import kotlinx.coroutines.*
import kotlinx.coroutines.test.*

class Debouncer(private val scope: CoroutineScope, private val waitMs: Long, private val action: (String) -> Unit) {
    private var pending: Job? = null
    fun submit(value: String) {
        pending?.cancel()
        pending = scope.launch { delay(waitMs); action(value) }
    }
}

fun main() = runTest {
    val saved = mutableListOf<String>()
    val debouncer = Debouncer(this, 300, saved::add)
    debouncer.submit("h")
    debouncer.submit("he")
    debouncer.submit("hey")
    advanceTimeBy(299); runCurrent()
    check(saved.isEmpty())
    advanceTimeBy(1); runCurrent()
    check(saved == listOf("hey"))
    println("saved $saved at virtual $currentTime ms")
}
Result
saved [hey] at virtual 300 ms
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and the jars kotlinx-coroutines-core-jvm-1.10.2 and kotlinx-coroutines-test-jvm-1.10.2
  2. Save as main.kt; run kotlinc -cp <both jars> main.kt -d out && kotlin -cp out:<both jars> MainKt
  3. In a project the same body goes in a @Test fun with testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core and -test 1.10.2 on 2026-11-11: the output matched the result shown.

  • kotlinx-coroutines-core
  • kotlinx-coroutines-test
  • kotlin >=2.0
  • kotlinx-coroutines >=1.10
  • jvm >=17

Design for testability — inject the clock

A `fun interface` makes the seam a lambda in tests.

fun interface Clock { fun nowMs(): Long }

class Session(private val clock: Clock, private val ttlMs: Long) {
    private val started = clock.nowMs()
    fun expired() = clock.nowMs() - started >= ttlMs
}

fun main() {
    var now = 1_000L
    val session = Session({ now }, ttlMs = 500)
    check(!session.expired())
    now += 500                             // the test owns time: no sleeping
    check(session.expired())
    println("expired after ${now - 1_000} ms of fake time")
}
Result
expired after 500 ms of fake time
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

A JUnit 5 test class

Backtick names read as sentences; `assertThrows<T>` is JUnit's Kotlin extension.

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

fun slugify(title: String): String {
    require(title.isNotBlank()) { "blank title" }
    return title.trim().lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-')
}

class SlugTest {
    @Test
    fun `joins words with single hyphens`() = assertEquals("hello-world", slugify("  Hello, World!  "))

    @Test
    fun `rejects blank titles`() {
        assertThrows<IllegalArgumentException> { slugify("   ") }
    }
}
Result

./gradlew test reports both tests passed.

Setup
  1. A Gradle JVM project with testImplementation(platform("org.junit:junit-bom:5.10.1")), testImplementation("org.junit.jupiter:junit-jupiter") and tasks.test { useJUnitPlatform() }
  2. Put the class under src/test/kotlin and run ./gradlew test

Compiled with kotlinc 2.2.20, JVM 17.0.15, junit-jupiter-api 5.10.1 on 2026-11-11; not run here — running it needs a JUnit Platform launcher (Gradle test) to run the tests.

  • JUnit 5
  • kotlin >=2.0
  • junit-jupiter >=5.10
  • jvm >=17

Sources: JUnit 5 User Guide

Principles worth stating

The same in every language.

  • Fakes you own (an in-memory repository, a Clock) over mocks of code you do not.
  • Inject dispatchers and scopes; in tests pass StandardTestDispatcher or the TestScope itself.
  • Thread.sleep or real delay in tests: slow and flaky — use virtual time.
  • runTest fails a test that leaves coroutines running — a free leak check.

Kotlin Language Essentials

Null safety, sealed hierarchies and when, generics and variance, exceptions, data classes, extensions and scope functions.

  • jvm
  • android
  • kotlin >=2.0
  • jvm >=17
  • Kotlin/JVM

Sources: Kotlin language guide — Kotlin docs

Swift idea → Kotlin

Close cousins, with a different default for classes (final) and for equality (`==` calls `equals`).

SwiftKotlinWatch out
Optional<T>, ?., ??, guard letT?, ?., ?:, ?: return!! is the force unwrap; Java types arrive as platform types (T!)
enum with associated valuessealed interface + data class caseswhen is exhaustive over a sealed type
structdata class (with vals)a reference type: copy() instead of value semantics
protocol + extensioninterface + extension functionsextensions are resolved statically
generics where<T : Bound>, where T : A, T : Bdeclaration-site variance: out / in
throwsunchecked exceptionsnothing in the signature says a function throws
defertry { } finally { }, use { }use closes a Closeable

Null safety

Nullability is part of the type. `?.` short-circuits, `?:` supplies a default — or returns early.

data class Address(val city: String?)
data class User(val name: String, val address: Address?)

fun cityOf(user: User): String {
    val city = user.address?.city ?: return "no city for ${user.name}"   // like guard let … else
    return city.uppercase()                                              // smart cast: String here
}

fun main() {
    val user = User("Ana", address = null)
    println(user.address?.city ?: "unknown city")
    println(cityOf(user))
    println(cityOf(User("Bo", Address("lima"))))
}
Result
unknown city
no city for Ana
LIMA
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Null safety — Kotlin docs

Sealed hierarchies and when

A new case breaks compilation at every `when` that does not handle it.

sealed interface Shape
data class Circle(val radius: Double) : Shape
data class Rect(val width: Double, val height: Double) : Shape

fun area(s: Shape): Double = when (s) {
    is Circle -> Math.PI * s.radius * s.radius
    is Rect -> s.width * s.height               // smart cast: s is a Rect here
}

fun main() {
    println(area(Rect(2.0, 3.0)))
    println(Math.round(area(Circle(1.0)) * 100) / 100.0)
}
Result
6.0
3.14
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Sealed classes and interfaces — Kotlin docs

Generics, bounds and variance

`out T` marks a producer, so a `Source<Int>` is a `Source<Number>` — Swift has no user-declared variance.

fun <T : Comparable<T>> clamp(value: T, low: T, high: T): T = maxOf(low, minOf(value, high))

interface Source<out T> { fun next(): T }

fun sumAll(sources: List<Source<Number>>): Double = sources.sumOf { it.next().toDouble() }

fun main() {
    println("${clamp(15, 0, 10)} ${clamp("m", "a", "f")}")
    val ints = object : Source<Int> { override fun next() = 2 }
    val doubles = object : Source<Double> { override fun next() = 0.5 }
    println(sumAll(listOf(ints, doubles)))
}
Result
10 f
2.5
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Generics — Kotlin docs

Exceptions, require and runCatching

`throw` is an expression; `require` throws `IllegalArgumentException`; `runCatching` turns a throw into a `Result`.

class InvalidPort(text: String) : Exception("not a port: $text")

fun parsePort(text: String): Int {
    val n = text.toIntOrNull() ?: throw InvalidPort(text)
    require(n in 1..65535) { "out of range: $n" }
    return n
}

fun main() {
    for (input in listOf("8080", "http", "70000")) {
        val result = runCatching { parsePort(input) }
        println(result.fold({ "port $it" }, { "${it::class.simpleName}: ${it.message}" }))
    }
}
Result
port 8080
InvalidPort: not a port: http
IllegalArgumentException: out of range: 70000
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Exceptions — Kotlin docs

Data classes and copies

`==` compares values, `===` identity; `copy()` is shallow, like a struct copy of a reference field.

data class Settings(val theme: String, val tags: List<String>)

fun main() {
    val a = Settings("dark", listOf("x"))
    val b = a.copy(theme = "light")
    println("$a $b")
    println("${a == a.copy()} ${a === a.copy()} ${a.tags === b.tags}")
}
Result
Settings(theme=dark, tags=[x]) Settings(theme=light, tags=[x])
true false true
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Extensions and scope functions

`apply` configures and returns the receiver; `let` transforms and returns the lambda's result.

fun String.initials(): String =
    split(" ").filter { it.isNotEmpty() }.joinToString("") { it.first().uppercase() }

data class Request(var url: String = "", var retries: Int = 0)

fun main() {
    println("ada lovelace".initials())
    val request = Request().apply { url = "https://example.com"; retries = 3 }
    println(request.let { "${it.url} x${it.retries}" })
}
Result
AL
https://example.com x3
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Scope functions — Kotlin docs

Flow — Core

Cold flows, StateFlow and SharedFlow as subjects, the operators that matter, lifetime, and bridging listeners.

  • jvm
  • android
  • kotlinx-coroutines-core
  • kotlin >=2.0
  • kotlinx-coroutines >=1.10
  • jvm >=17
  • Kotlin/JVM with kotlinx.coroutines

Sources: Asynchronous Flow — Kotlin docs · StateFlow and SharedFlow — Android Developers

Combine → Flow

The same reactive vocabulary, built on suspension instead of demand.

CombineFlowNote
PublisherFlow<T> (cold)the block runs again for every collector
CurrentValueSubjectMutableStateFlowalways has a value; equal values are not re-emitted
PassthroughSubjectMutableSharedFlowno value; replay and buffers are configurable
sink + AnyCancellablecollect / launchIn(scope)the scope's end cancels the collection
switchToLatestflatMapLatestcancels the previous inner flow
zip / combineLatestzip / combine—
receive(on:)flowOn(dispatcher)changes the upstream context, not the collector's
share()shareIn / stateInturns a cold flow hot, with an explicit start policy

Cold flows

A `flow { }` does nothing until collected, and runs again for each collector.

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun numbers(): Flow<Int> = flow {
    println("start")
    for (i in 1..3) emit(i)
}

fun main() = runBlocking {
    numbers().map { it * 10 }.collect { println(it) }
    println(numbers().toList())
}
Result
start
10
20
30
start
[1, 2, 3]
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

StateFlow and SharedFlow

`StateFlow` holds the latest value and skips equal ones; `SharedFlow` broadcasts events.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val count = MutableStateFlow(0)
    val events = MutableSharedFlow<String>()

    val watcher = launch { count.collect { println("count = $it") } }
    val listener = launch { events.collect { println("event: $it") } }
    yield()                                    // let both collectors subscribe
    count.value = 1
    yield()
    count.value = 1                            // equal: not re-emitted
    events.emit("saved")
    yield()
    watcher.cancel()
    listener.cancel()
}
Result
count = 0
count = 1
event: saved
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Operators that matter

`flatMapLatest` cancels stale work — the search-as-you-type operator.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runBlocking {
    flowOf("a", "ab", "abc")
        .onEach { delay(50) }                  // a keystroke every 50 ms
        .flatMapLatest { q -> flow { delay(500); emit("results for $q") } }   // a slow search
        .collect { println(it) }

    println(flowOf("ana", "bo").zip(flowOf(31, 27, 99)) { n, a -> "$n:$a" }.toList())
    println(flowOf(1, 1, 2, 2, 3).distinctUntilChanged().scan(0) { acc, x -> acc + x }.toList())
}
Result
results for abc
[ana:31, bo:27]
[0, 1, 3, 6]
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Lifetime belongs to a scope

`launchIn(scope)` ties the collection to the scope; cancelling the scope ends it (the role of `AnyCancellable`).

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun ticker(periodMs: Long): Flow<Int> = flow {
    var n = 0
    while (true) {
        emit(n++)
        delay(periodMs)
    }
}

fun main() = runBlocking {
    val screen = CoroutineScope(coroutineContext + Job())   // stand-in for viewModelScope
    val thirdTick = CompletableDeferred<Unit>()
    ticker(100).onEach { println("tick $it"); if (it == 2) thirdTick.complete(Unit) }.launchIn(screen)
    thirdTick.await()                                        // e.g. the screen goes away after three ticks
    screen.cancel()
    delay(200)
    println("after cancel: quiet")
}
Result
tick 0
tick 1
tick 2
after cancel: quiet
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Bridging a listener API

`callbackFlow` registers on collect and unregisters in `awaitClose` when the collector stops.

import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun interface Listener { fun onReading(value: Int) }

class Sensor {                                 // a listener API you do not own
    fun register(listener: Listener) {
        Thread { for (v in listOf(10, 20, 30, 40)) listener.onReading(v) }.start()
    }
    fun unregister() = println("unregistered")
}

fun Sensor.readings(): Flow<Int> = callbackFlow {
    register { value -> trySend(value) }
    awaitClose { unregister() }
}

fun main() = runBlocking {
    println(Sensor().readings().take(3).toList())
}
Result
unregistered
[10, 20, 30]
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Sources: callbackFlow — kotlinx.coroutines API

Gotchas

The ones that cost a day.

  • collect suspends until the flow completes — a StateFlow never does, so the next line never runs.
  • Updating a StateFlow with a mutated list: equal by equals, so nothing is emitted. Emit a new list.
  • stateIn(scope, SharingStarted.WhileSubscribed(5_000), initial) for UI state: stops upstream work when nobody watches.
  • flowOn(Dispatchers.IO) for blocking upstream work; the collector keeps its own context.
  • catch { } handles upstream exceptions only — a throw in collect { } is not caught by it.

Shared Mutable State — Mutex, Confinement, Actors

Three ways to keep coroutines on many threads from racing — none of them checked by the compiler.

  • jvm
  • android
  • kotlinx-coroutines-core
  • kotlin >=2.0
  • kotlinx-coroutines >=1.10
  • jvm >=17
  • Kotlin/JVM with kotlinx.coroutines

Sources: Shared mutable state and concurrency — Kotlin docs

Swift isolation → Kotlin

Swift proves data-race freedom at compile time; Kotlin leaves it to discipline and these tools.

SwiftKotlinDifference
actorstate owned by one coroutine that reads a channel (below)one message at a time, start to finish: no reentrancy at its suspension points
actor (serial executor)a limitedParallelism(1) dispatcherserial only between suspension points: other work interleaves at an await, as on a Swift actor
actor-isolated mutationMutex.withLock { }suspends while waiting, never blocks a thread
Sendable—no compiler check: immutable data (val, read-only collections) by convention
@MainActorDispatchers.Maina dispatcher choice, not a type-system guarantee
atomic countersjava.util.concurrent.atomic.*same as Synchronization.Atomic

Mutex and atomics

100,000 increments from many threads. A plain `count++` would lose updates; both of these do not.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.atomic.AtomicInteger

fun main() = runBlocking {
    val mutex = Mutex()
    var guarded = 0
    val atomic = AtomicInteger()
    withContext(Dispatchers.Default) {         // a pool of threads
        repeat(1_000) {
            launch {
                repeat(100) {
                    mutex.withLock { guarded++ }
                    atomic.incrementAndGet()
                }
            }
        }
    }
    println("guarded=$guarded atomic=${atomic.get()}")
}
Result
guarded=100000 atomic=100000
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Confinement to one-at-a-time

`limitedParallelism(1)` gives a serial view of a shared pool — the closest thing to a serial executor.

import kotlinx.coroutines.*

val counterContext = Dispatchers.Default.limitedParallelism(1)

fun main() = runBlocking {
    var count = 0                               // only ever touched on counterContext
    withContext(Dispatchers.Default) {
        repeat(1_000) { launch { withContext(counterContext) { count++ } } }
    }
    println(count)
}
Result
1000
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

An actor from a channel

One coroutine owns the state and handles one message at a time; replies come back through `CompletableDeferred`.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

sealed interface AccountMsg {
    data class Deposit(val amount: Int) : AccountMsg
    data class Balance(val reply: CompletableDeferred<Int>) : AccountMsg
}

fun CoroutineScope.account(): Channel<AccountMsg> {
    val inbox = Channel<AccountMsg>(Channel.UNLIMITED)
    launch {
        var balance = 0
        for (msg in inbox) when (msg) {
            is AccountMsg.Deposit -> balance += msg.amount
            is AccountMsg.Balance -> msg.reply.complete(balance)
        }
    }
    return inbox
}

fun main() = runBlocking {
    val account = account()
    coroutineScope {
        repeat(100) { launch(Dispatchers.Default) { account.send(AccountMsg.Deposit(10)) } }
    }
    val reply = CompletableDeferred<Int>()
    account.send(AccountMsg.Balance(reply))
    account.close()                             // the actor drains its inbox, then stops
    println(reply.await())
}
Result
1000
Setup
  1. A Kotlin 2.x compiler (kotlinc), JDK 17 or later, and kotlinx-coroutines-core-jvm-1.10.2.jar
  2. Save as main.kt; run kotlinc -cp kotlinx-coroutines-core-jvm-1.10.2.jar main.kt -d out && kotlin -cp out:kotlinx-coroutines-core-jvm-1.10.2.jar MainKt
  3. Or, in a Gradle project: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

Ran with kotlinc 2.2.20, JVM 17.0.15, kotlinx-coroutines-core 1.10.2 on 2026-11-11: the output matched the result shown.

Sources: Channels — Kotlin docs

Rules

Without a compiler check, the design has to make races impossible.

  • Prefer immutable data passed between coroutines: val properties, read-only collections, data class copies.
  • Expose state as StateFlow and mutate it only with update { } — an atomic compare-and-set.
  • synchronized or ReentrantLock around a suspension point: the lock is held while the coroutine is parked.
  • @Volatile makes writes visible, not compound updates atomic: x++ still races.

JVM — Files, Text & Atomic I/O

kotlin.io.path over java.nio — small files, atomic replace with fsync, streaming lines, journals, walking trees.

  • jvm
  • kotlin >=2.0
  • jvm >=17
  • Kotlin/JVM (kotlin.io.path and java.nio.file)

Sources: kotlin.io.path — Kotlin API reference · Files — Java SE 17 API

Read and write a small text file

Whole-file helpers are right for small files; they default to UTF-8.

import kotlin.io.path.*

fun main() {
    val dir = createTempDirectory("demo")
    val file = dir / "notes.txt"
    file.writeText("first line\nsecond line\n")
    println("${file.readLines().size} lines, ${file.fileSize()} bytes")
    dir.toFile().deleteRecursively()
}
Result
2 lines, 23 bytes
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar — it works in a fresh temporary directory and removes it

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Writing atomically

Write a temporary sibling, `force(true)` it to the device, then `ATOMIC_MOVE` over the target.

import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import kotlin.io.path.*

fun writeAtomically(target: Path, text: String) {
    val temp = Files.createTempFile(target.parent, ".${target.name}", ".tmp")   // same file system
    FileChannel.open(temp, StandardOpenOption.WRITE).use { channel ->
        val buffer = ByteBuffer.wrap(text.toByteArray())
        while (buffer.hasRemaining()) channel.write(buffer)
        channel.force(true)                    // on the device before the move publishes it
    }
    Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE)
}

fun main() {
    val dir = createTempDirectory("atomic")
    val settings = dir / "settings.json"
    writeAtomically(settings, """{"theme":"dark"}""")
    writeAtomically(settings, """{"theme":"light"}""")
    println(settings.readText())
    dir.toFile().deleteRecursively()
}
Result
{"theme":"light"}
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar — it works in a fresh temporary directory and removes it

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Sources: Files.move — Java SE 17 API

Streaming a large file line by line

`useLines` hands a lazy sequence and closes the file afterwards; one line is in memory at a time.

import kotlin.io.path.*

fun main() {
    val log = createTempFile("app", ".log")
    log.bufferedWriter().use { out ->
        for (i in 0 until 10_000) out.appendLine("${if (i % 10 == 0) "ERROR" else "INFO"} event $i")
    }
    val errors = log.useLines { lines -> lines.count { it.startsWith("ERROR") } }
    println("$errors errors")
    log.deleteExisting()
}
Result
1000 errors
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar — it works in a fresh temporary directory and removes it

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

An append-only journal

One JSON object per line — appending never rewrites what is already there.

import kotlin.io.path.*

fun main() {
    val journal = createTempFile("journal", ".jsonl")
    for (event in listOf("start", "save", "stop")) journal.appendText("{\"event\":\"$event\"}\n")
    println(journal.readLines().map { it.substringAfter(":\"").removeSuffix("\"}") })
    journal.deleteExisting()
}
Result
[start, save, stop]
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar — it works in a fresh temporary directory and removes it

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Walking a directory tree

`walk()` is lazy and depth-first; it is still marked experimental, hence the opt-in.

import kotlin.io.path.*

@OptIn(ExperimentalPathApi::class)
fun main() {
    val root = createTempDirectory("tree")
    (root / "src/main").createDirectories()
    (root / "src/main/App.kt").writeText("fun main() {}")
    (root / "README.md").writeText("# demo")
    val sources = root.walk().filter { it.extension == "kt" }.map { it.relativeTo(root).invariantSeparatorsPathString }
    println(sources.toList())
    root.deleteRecursively()
}
Result
[src/main/App.kt]
Setup
  1. A Kotlin 2.x compiler (kotlinc) and JDK 17 or later
  2. Save as main.kt; run kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar — it works in a fresh temporary directory and removes it

Ran with kotlinc 2.2.20, JVM 17.0.15 on 2026-11-11: the output matched the result shown.

Rules that prevent data loss

The JVM adds a few of its own.

  • Close every stream with use { } — it is defer for Closeable.
  • Replace a file with a temporary sibling + ATOMIC_MOVE; never truncate the original first.
  • Java's FileReader / InputStreamReader without a charset used the platform default before JDK 18; Kotlin's readText() / readLines() default to UTF-8 already.
  • ATOMIC_MOVE over an existing target is implementation-specific: POSIX rename replaces, other file systems may throw.
  • On Android, app-private files go under context.filesDir; shared storage goes through the Storage Access Framework.

Jetpack Compose — State & Data Flow

remember and rememberSaveable, state hoisting, ViewModel + StateFlow, lifecycle-aware collection, effects.

  • android
  • Jetpack Compose
  • androidx.lifecycle
  • kotlin >=2.0
  • compose-bom >=2024.09.00
  • lifecycle >=2.8
  • android API 21+
  • an Android app with Jetpack Compose

Sources: State and Jetpack Compose — Android Developers · ViewModel overview — Android Developers

SwiftUI → Compose

The decision table, translated.

SwiftUIComposeLives until
@Stateremember { mutableStateOf(x) }the composable leaves the composition
@SceneStorage (survives recreation)rememberSaveable { … }the saved-state registry is cleared (also survives rotation)
@Bindingstate hoisting: value + onValueChange—
@Observable modelViewModel exposing StateFlowthe screen's back-stack entry is destroyed
@EnvironmentCompositionLocal—
.task(id:)LaunchedEffect(key)the key changes or the composable leaves
.onDisappearDisposableEffect { onDispose { } }—

remember, rememberSaveable and hoisting

A stateless composable takes the value and an event; the caller owns the state.

@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {        // stateless: easy to preview and test
    Button(onClick = onIncrement) { Text("Clicked $count times") }
}

@Composable
fun CounterScreen() {
    var count by rememberSaveable { mutableIntStateOf(0) }   // survives recomposition and rotation
    Counter(count = count, onIncrement = { count++ })
}
Result

The button's label counts up on each tap and keeps its value when the device rotates.

Setup
  1. An Android Studio project with the Compose template (Kotlin 2.x, the Compose BOM and androidx.activity:activity-compose)
  2. Call the screen from setContent { } in your ComponentActivity, then run the app on a device or emulator

Excerpt, not compiled or run here: it needs an Android app with Jetpack Compose (needs the Android SDK and the Compose compiler plugin).

ViewModel + StateFlow, collected with the lifecycle

One immutable UI state, updated atomically; the screen collects it only while it is at least STARTED.

data class SearchUiState(val query: String = "", val results: List<String> = emptyList(), val loading: Boolean = false)

class SearchViewModel(private val repo: SearchRepository) : ViewModel() {
    private val _state = MutableStateFlow(SearchUiState())
    val state: StateFlow<SearchUiState> = _state.asStateFlow()
    private var searchJob: Job? = null

    fun onQueryChange(query: String) {
        _state.update { it.copy(query = query, loading = true) }
        searchJob?.cancel()                                  // a newer query supersedes the old one
        searchJob = viewModelScope.launch {                  // cancelled when the ViewModel is cleared
            val results = repo.search(query)
            _state.update { it.copy(results = results, loading = false) }
        }
    }
}

@Composable
fun SearchScreen(vm: SearchViewModel) {
    val state by vm.state.collectAsStateWithLifecycle()
    Column {
        TextField(value = state.query, onValueChange = vm::onQueryChange)
        if (state.loading) CircularProgressIndicator()
        state.results.forEach { Text(it) }
    }
}
Result

Typing updates the field at once, shows a spinner, then the results of the latest query only.

Setup
  1. An Android Studio project with the Compose template, plus androidx.lifecycle:lifecycle-viewmodel-compose and lifecycle-runtime-compose
  2. A SearchRepository with suspend fun search(query: String): List<String>, passed to the ViewModel through a factory or your DI container

Excerpt, not compiled or run here: it needs an Android app with Jetpack Compose (needs the Android SDK and the Compose compiler plugin).

Sources: StateFlow and SharedFlow — Android Developers · collectAsStateWithLifecycle — Android Developers

Async work tied to the screen

`LaunchedEffect(key)` starts when the composable enters, restarts when the key changes and is cancelled when it leaves — `.task(id:)`.

@Composable
fun Profile(userId: String, load: suspend (String) -> String) {
    var name by remember(userId) { mutableStateOf<String?>(null) }   // reset for a new user
    LaunchedEffect(userId) {
        name = load(userId)
    }
    Text(name ?: "Loading…")
}
Result

Shows "Loading…", then the name; switching userId cancels the old load and starts a new one.

Setup
  1. An Android Studio project with the Compose template (Kotlin 2.x, the Compose BOM and androidx.activity:activity-compose)
  2. Call the screen from setContent { } in your ComponentActivity, then run the app on a device or emulator

Excerpt, not compiled or run here: it needs an Android app with Jetpack Compose (needs the Android SDK and the Compose compiler plugin).

Sources: Side-effects in Compose — Android Developers

Identity and lifetime rules

Most Compose state bugs are one of these.

  • Mutating a list inside mutableStateOf(list) — Compose sees the same object and does not recompose. Assign a new list, or use mutableStateListOf.
  • Side effects (network, logging, writes) directly in a composable body: it runs on every recomposition.
  • Give items(…, key = { it.id }) stable keys so state follows the item, not the position.
  • derivedStateOf { } for values computed from state that changes more often than the result.
  • Configuration changes recreate the activity: remember resets, rememberSaveable and the ViewModel survive.

Coverage against the Swift Catalog

Every Swift topic, and what this catalog has for it — an equivalent, a partial counterpart, or the reason there is none.

How to read the matrix

Nothing here is a translation made to fill a gap.

  • equivalent — a sheet here covers the same concept and shares its concept id, so the language selector lands on it from the Swift sheet.
  • partial — a sheet here covers part of it under its own concept id, so the selector does not land on it: the Here column names it and the reason says what is missing.
  • no counterpart — nothing here; the reason says whether the topic does not exist in this language or is simply not written yet.

Swift topic by topic

In the Swift catalog's reading order.

Swift topicDispositionHereWhy
SwiftUI — State & Data FlowequivalentJetpack Compose — State & Data Flowthe same decision: local state, hoisting, an observable model, environment and view-scoped tasks
SwiftUI — Views, Layout & Animationno counterpart—Compose layout and animation are not written up yet
SwiftUI vs UIKitno counterpart—the analogous pair, Compose vs Android Views, is not written up yet
TCA — Core LooppartialJetpack Compose — State & Data Flowunidirectional flow with one immutable UI state in a ViewModel; no reducer or effect library like TCA
TCA — Composition, Dependencies, Testingno counterpart—a Swift library's API
TCA + SwiftUI — Navigation & UIno counterpart—a Swift library's API; Navigation Compose is not written up yet
Modern Swift Concurrency — CoreequivalentCoroutines — Coresuspend functions, structured fan-out, failure propagation, cancellation and callback bridging
Actors, Sendable & IsolationpartialShared Mutable State — Mutex, Confinement, ActorsMutex, confinement and channel-owned state; Kotlin has no compiler-checked isolation or Sendable
Swift 6 Concurrency — Do's & Don'tsno counterpart—about migrating to Swift 6's strict concurrency checking
Multithreading — GCD vs Combine vs async/awaitpartialCoroutines — Corecoroutines and dispatchers only; the threads/executors vs RxJava vs coroutines comparison is not drawn
Combine — CoreequivalentFlow — Corecold streams, subjects, operators, lifetime and bridging, with a Combine → Flow table
Timers & Async Patterns — Combine vs async/awaitpartialFlow — Corea ticker, cancellation by scope and search-as-you-type; not the full Combine-vs-async catalogue
SwiftUI + Combine — Wiring Them TogetherpartialJetpack Compose — State & Data FlowcollectAsStateWithLifecycle is the one Flow → UI connection point; there is no second framework to wire
Timing Operators — Combine vs Swift ConcurrencypartialTesting Approaches in Kotlina debounce tested on virtual time; the Flow timing operators are not catalogued
Clean Code in Swiftno counterpart—not written yet; the principles are language-neutral and the Swift sheet's advice carries over
SOLID in Swiftno counterpart—not written yet; the principles are language-neutral
Functional Programming in SwiftpartialCollections — Operation Cookbookhigher-order functions and lazy sequences; no composition, currying or sealed-type modelling card
Swift Language EssentialsequivalentKotlin Language Essentialsthe same questions — optionals, enums, generics, errors, value vs reference — answered for Kotlin
Collections — Types & ProtocolsequivalentCollections — Lists, Sets, Maps & Arrayswhich container and what it costs
Collections — Operation CookbookequivalentCollections — Operation Cookbooktransform, fold, group, sort, slice, zip and sequences
Swift Engineering Best Practicesno counterpart—not written yet
Architectural Approachesno counterpart—not written yet; Android's architecture guide is the reference
Modularization Approachesno counterpart—not written yet; Gradle modules are a different mechanism from Swift packages
Testing Approaches on iOSequivalentTesting Approaches in Kotlintooling, virtual time, injected seams and JUnit 5
AI Workflows for Swift (Cursor + Claude)no counterpart—Swift-specific by design: it is about why Swift is harder for models and wiring Xcode to an agent
Algorithmic Patterns & ComplexityequivalentAlgorithmic Patterns & Complexitybuilt-in costs and five core patterns; fewer patterns than the Swift sheet
Streaming & Time-Series Patternsno counterpart—not written yet; the algorithms (ring buffers, sketches, reservoir sampling) are language-neutral
Files, Text & Atomic I/OequivalentJVM — Files, Text & Atomic I/Othe same file-safety questions on java.nio
Core Bluetooth (BLE)no counterpart—an Apple framework; Android's android.bluetooth.le is the counterpart and is not written up yet