Learning: Kotlin
Become the engineer trusted to own Kotlin on a team, in a backend service or an Android app: able to model a domain idiomatically, write and reason about coroutines and Flow, ship a typed, tested Kotlin service or Android component, and review someone's Kotlin and name concretely what a construct is costing them.
Latest lesson: 44. Testing Coroutines Beyond runTest
Success looks like
- Model a domain with data classes, sealed classes and enums, and know when null safety, not an exception, is the correct signal.
- Write and reason about coroutines and
Flow, and compare the model to Java virtual threads. - Ship a typed, tested Kotlin backend service or Android component, designed idiomatically.
- Given Kotlin written with a habit that merely compiles (defensive null checks instead of the type system, a callback instead of a coroutine), name the habit and rewrite it idiomatically.
Constraints
- Assumes no prior Kotlin. Experience in Java or another JVM language shortens the early stages but is not required, and it brings habits Kotlin punishes quietly: defensive null checks instead of the type system, a class where a data class or sealed type would say it better.
- Covers both backend/server-side and Android contexts, touching each where they diverge.
- Needs only a JDK, the Kotlin toolchain and a terminal; Android Studio only where Android-specific material needs it.
Out of scope
- JVM internals past what explains Kotlin's own compiled behaviour (full garbage-collector tuning, profiling, bytecode engineering): see
programming/java, linked to for further depth rather than retaught. - Other JVM languages (Java, Scala, Clojure, Groovy) as subjects in their own right, though a contrast appears where it clarifies a Kotlin idiom.
- Kotlin Multiplatform: targeting JS, Native, or Wasm, structuring
expect/actualdeclarations across source sets, and the platform-specific stdlib differences those targets introduce. This arc is JVM-only throughout, per the mission's own framing; a lesson here would need its own Gradle setup and its own set of idioms, not a JVM lesson with a multiplatform footnote.kotlinx.serialization(lesson 38) is the one place multiplatform reach is mentioned, as a documented reason for a JVM-side design choice, not as multiplatform material in its own right.
The arc
Eleven stages, zero to senior. Not a lesson list: a stage takes several lessons, and the boundaries are soft.
| Stage | Lessons | Covers | Done when |
|---|---|---|---|
| 1. Foundations | 0001 to 0006 | Null safety, val/var and immutability, values vs references, basic types and string templates, collections basics (List/Set/Map, mutable vs read-only), control flow (when, ranges, for) | Can predict nullability and mutability without running the code |
| 2. Modelling | 0007 to 0012 | Classes and properties, data classes, sealed classes and exhaustive when, enums, object declarations and companion objects, interfaces with default methods | Models a domain without reaching for a class-per-thing hierarchy first |
| 3. Idiom | 0013 to 0018 | Extension functions, scope functions (let/run/with/apply/also), higher-order functions and lambdas, inline functions and reified generics, delegation (by), operator overloading | Writes Kotlin a reviewer would not describe as translated Java |
| 4. Collections and sequences | 0019 to 0021 | Kotlin's collection operators, lazy Sequence vs eager collections, grouping and folding | Chooses between a collection pipeline and a sequence and can defend the cost of each |
| 5. Concurrency | 0022 to 0027 | Threads and the JVM memory model basics, suspend functions and coroutine builders, structured concurrency, coroutine context and dispatchers, Flow, cancellation and exception handling | Can predict what a concurrent coroutine program does before running it, and compare the model to Java virtual threads |
| 6. Testing and build | 0028 to 0030 | Kotlin test frameworks, mocking, the Gradle Kotlin DSL and dependency management | Someone else can clone, build, test and run it |
| 7. Shipping a service | 0031 to 0033 | Structuring a typed, tested backend service, Android-specific idioms where the platform diverges, generics and variance (in/out) | Has structured a typed, tested Kotlin backend service or Android component; stage 10 completes it with an HTTP layer, configuration, logging and persistence |
| 8. Judgment | 0034 to 0035 | Java interop, reviewing Kotlin and naming precisely what a construct is costing | Trusted to make the call and explain it to someone else |
| 9. Advanced Idiom | 0036 to 0039 | Type-safe builders and DSLs, value class/@JvmInline, kotlinx.serialization, Result and error-handling idioms | Writes a small type-safe builder DSL, a zero-cost wrapper type, and a Result-based error path idiomatically |
| 10. Completing the Service | 0040 to 0043 | An HTTP layer, configuration, logging, and persistence for the Kotlin backend service stage 7 started structuring | Ships a Kotlin backend service with a routed HTTP layer, externalized configuration, structured logging, and database access, not only a structured shell |
| 11. Testing Coroutines Beyond runTest | 0044 | Turbine's cancelAndIgnoreRemainingEvents() for a flow that never completes, and what a cancellation test actually has to assert given that cancellation is cooperative | Tests an infinite flow without hanging, and tests cancellation by its actual, documented evidence rather than a Job flag alone |
Lessons
Work through these in order.
| # | Lesson | Teaches |
|---|---|---|
| 0001 | Null Safety | Moving "could this be null" from a runtime surprise to a compile-time question, and how the Java habit undoes it |
| 0002 | val/var and Immutability | Why val is the default worth defending, what it actually guarantees (and doesn't), and the Java habit of reaching for var out of reflex |
| 0003 | Values vs References | Why == means something different in Kotlin than in Java, and the specific bug the Java habit of writing == produces |
| 0004 | Basic Types and String Templates | Why Kotlin has no primitives at the language level, and string templates as a language feature rather than concatenation with extra syntax |
| 0005 | Collections Basics | List, Set, and Map as read-only-by-interface collections, and why a mutable collection held by a val is still mutable |
| 0006 | Control Flow | if and when as expressions that produce values, what a range actually is, and the stage 1 capstone of predicting nullability and mutability without running code |
| 0007 | Classes and Properties | Properties as the replacement for Java's getter/setter boilerplate, and why the Kotlin docs themselves say to reach for a class last |
| 0008 | Data Classes | What data class actually generates, and the subtle rule that only primary-constructor properties participate in any of it |
| 0009 | Sealed Classes and Exhaustive when | How sealing a hierarchy turns a when's exhaustiveness from a manual promise into a compiler-checked guarantee |
| 0010 | Enums | Each enum constant as a real object with its own state and behavior, and precisely where an enum fits over a sealed class |
| 0011 | Object Declarations and Companion Objects | Singletons declared, not implemented by hand, and companion objects as Kotlin's actual replacement for Java's static members |
| 0012 | Interfaces with Default Methods | Why a Kotlin interface can implement behavior but never hold state, and how to resolve a diamond conflict explicitly |
| 0013 | Extension Functions | Adding behavior to a type without inheritance, and the static-dispatch gotcha that trips people up the first time they hit it |
| 0014 | Scope Functions | Choosing among let, run, with, apply, and also by what each one returns and how it exposes the object, not by habit |
| 0015 | Higher-Order Functions and Lambdas | Function types as real types, trailing-lambda syntax, and the receiver mechanism scope functions are actually built on |
| 0016 | Inline Functions and Reified Generics | Why inline exists to remove a real cost, why it enables non-local returns, and why reified type parameters only work because of inlining |
| 0017 | Delegation | Implementing an interface by forwarding to a held object with zero boilerplate, and reusable property behavior via by lazy and custom delegates |
| 0018 | Operator Overloading | Operators as ordinary named functions with a fixed symbol mapping, and the actual discipline of implementing one only where it means what the symbol already means |
| 0019 | Collection Operators | The collection operators as extension functions that return a new collection eagerly, and the Java Stream habit that misprices a chain of them |
| 0020 | Sequences and Laziness | Why the same operator chain behaves differently on a Sequence, what a terminal operation is actually for, and when laziness costs more than it saves |
| 0021 | Grouping and Folding | Aggregating with fold and reduce, grouping without building the groups, and the stage 4 capstone of defending what a pipeline costs |
| 0022 | Threads and the Memory Model | Why Kotlin has no memory model of its own on the JVM, the three constructs it spells as annotations instead of keywords, and the two guarantees a val does not give you |
| 0023 | Suspend Functions and Coroutine Builders | What suspend actually changes about a function, the three builders and what each one returns, and why suspending code still reads top to bottom |
| 0024 | Structured Concurrency | The parent and child tree that makes cancellation predictable, why a scope you own is a scope you must cancel, and what GlobalScope actually costs |
| 0025 | Coroutine Context and Dispatchers | The context as a set of elements combined with plus, which dispatcher belongs to which kind of work, and the one context element that silently breaks structured concurrency |
| 0026 | Flows | A flow as the asynchronous third option after a list and a sequence, what cold actually means for each collector, and the context rule a flow builder enforces at runtime |
| 0027 | Cancellation and Exception Handling | Why cancellation is cooperative and what makes code ignore it, why a CancellationException is not a failure, and how the coroutine model compares with Java virtual threads |
| 0028 | Test Frameworks | The layers a Kotlin test actually sits on, why runTest's virtual time stops at the dispatcher boundary, and what that forces on the design of the code under test |
| 0029 | Mocking | Why final-by-default changes what mocking means in Kotlin, what MockK does for the constructs that are not methods on an object, and when finality is telling you a seam is missing |
| 0030 | Gradle and Dependency Management | The Kotlin DSL's type-safe accessors and where they run out, why a Gradle configuration is not a Maven scope, and the resolution rule that decides which version reaches the classpath |
| 0031 | Structuring a Service | Where Kotlin files actually go and what goes inside a class, and the four decisions from earlier stages that decide whether a service is testable |
| 0032 | Android Divergences | The scopes Android owns on your behalf, main-safety as a contract the callee keeps, and why composition lifetime is not lifecycle lifetime |
| 0033 | Generics and Variance | Declaration-site variance in place of wildcards, what out and in actually promise about a type parameter, and how to read a projected signature |
| 0034 | Java Interop | Why the boundary is asymmetric, what a platform type costs you and how to pay it back, and which annotations you owe a Java caller |
| 0035 | Reviewing Kotlin | Turning the arc into a review instrument: the cost each habit hides, the shape of a comment that names it, and what not to review |
| 0036 | Type-Safe Builders and DSLs | Lesson 15 taught the receiver-style function type that lets run and apply expose this; a type-safe builder is that exact mechanism nested recursively, and @DslMarker is the one attribute that keeps a nested builder from silently reaching the wrong receiver |
| 0037 | value class and @JvmInline | A value class is the fix for primitive obsession that the JVM usually lets you have for free, represented as its own underlying value with no wrapper object at all, except in the specific, documented cases where the compiler still has to box it |
| 0038 | kotlinx.serialization | A Java habit reaches for a library that inspects a class's shape through reflection at runtime; kotlinx.serialization generates that same logic at compile time instead, through a compiler plugin, which is exactly what lets it work on Kotlin targets that have no runtime reflection API at all |
| 0039 | Result and Error-Handling Idioms | runCatching wraps any Throwable into a Result, which is exactly the problem inside a coroutine, since lesson 27's cooperative cancellation depends on CancellationException propagating uncaught, and runCatching swallows it into an ordinary failure unless a caller explicitly rethrows it first |
| 0040 | HTTP Layer | Lesson 31 deliberately deferred routing to whichever framework you chose; Ktor's own routing block turns out to be lesson 36's type-safe builder DSL applied to HTTP, nothing installed by default, and request-parameter extraction that fails loudly rather than silently binding the wrong thing |
| 0041 | Configuration | A service's actual behavior, its port, its database URL, its secrets, has to change across environments without recompiling a single line, and Ktor's configuration file plus one specific, easy-to-miss substitution idiom is how a default and an environment override live in exactly one place |
| 0042 | Logging | SLF4J silently does nothing until a real logging backend is added as a dependency, and CallLogging's per-request MDC values are silently invisible until the log pattern itself is updated to print them, the same shape of trap appearing twice in the same lesson |
| 0043 | Persistence | Exposed's DSL is another type-safe builder, and it is mostly blocking underneath, since it wraps JDBC; the stage 10 capstone is dispatching that blocking work correctly instead of stalling whatever thread happened to be running the request, and revisiting what stage 7's shipped service actually needed all along |
| 0044 | Testing Coroutines Beyond runTest | Lesson 28 taught Turbine's awaitItem and the hang it causes if nothing terminates it; this lesson covers the specific tool for a flow that never terminates on its own, and what a cancellation test actually has to assert, given that cancellation is cooperative and does not update everything the instant cancel() is called |
Reference
- Glossary: canonical terms for this topic
- Resources: trusted sources
- Null Safety and Mutability: null safety,
val/var, values vs references, predicting nullability and mutability - Modelling: data classes, sealed classes and exhaustive
when, object declarations and companion objects, interfaces with default methods - Idiom: extension functions, scope functions (
let/run/with/apply/also), delegation (by), operator overloading - Collections and Sequences: Kotlin's collection operators, lazy
Sequencevs eager collections, grouping and folding - Concurrency: suspend functions and coroutine builders, structured concurrency, coroutine context and dispatchers,
Flow, comparison to Java virtual threads - Testing and Build: Kotlin test frameworks, mocking, the Gradle Kotlin DSL and dependency management
- Shipping a Service: structuring a typed, tested backend, Android-specific idioms, generics and variance
- Judgment: Java interop, reviewing Kotlin and naming precisely what a construct is costing
How this works
Each lesson is short and self-contained. Answer keys are collapsed: recall first, then open them. The real-world reps matter more than the reading, and spacing them out is the point. Anything still unclear at the end of a lesson is worth chasing to its primary source before moving on.