Before You Start
Built by JetBrains to fix what its own engineers found frustrating about Java, then adopted by Google as Android's preferred language. These two lessons explain why, and get a compiler installed.
What is Kotlin?
JetBrains built it to fix what its own engineers found frustrating about Java
Kotlin began as JetBrains' own internal answer to Java's friction points — verbosity, null pointer exceptions, checked exceptions — before Google made it Android's officially preferred language in 2019. That origin as a dogfooded internal tool, not a research project, explains why so much of Kotlin reads as pragmatic fixes rather than academic experiments.
Kotlin's origin at JetBrains, Google's 2019 Android endorsement, and where Kotlin 2.x's K2 compiler sits today.
Setup and Compiler
kotlinc compiles standalone files — but real projects almost always use Gradle
<code>kotlinc</code> can compile a standalone <code>.kt</code> file directly from the command line for learning and small scripts, but any real Kotlin project — especially Android or multiplatform work — reaches for Gradle instead, which this course covers later in the Build Tools lesson.
Installing kotlinc, compiling and running a first .kt file, and IDE setup (IntelliJ IDEA / Android Studio).
Language Fundamentals
The syntax and object model you will use in nearly every file: variables, control flow, functions, classes, interfaces, and the companion-object replacement for Java's static. Six lessons before Kotlin's type system properly begins.
Variables and Types
val makes a reference immutable — and it's the default nearly everyone reaches for first
Kotlin's <code>val</code> declares an immutable reference and <code>var</code> a reassignable one, with <code>val</code> treated as the idiomatic default nearly everywhere — a small syntactic nudge toward immutability that shapes how most real Kotlin code actually gets written.
val vs. var, Kotlin's basic types, type inference, and string templates.
Control Flow
if is an expression, not just a statement — and when replaces switch with genuinely more power
Kotlin's <code>if</code> can be used as an expression that directly produces a value, not merely a branching statement, and <code>when</code> replaces <code>switch</code> with real added power — matching ranges, types, and arbitrary boolean conditions, not just equality against a constant.
if/else as both statement and expression, when's range/type/arbitrary-condition matching, and for/while loops.
Functions
Default parameter values and named arguments remove most of the need for overloading
Default parameter values combined with named arguments cover most of the situations Java would force you to solve with method overloading — one function signature handles many call shapes, and named arguments make a call with several optional parameters genuinely readable at the call site.
Function syntax, default parameter values, named arguments, single-expression functions, and top-level functions.
Classes and Objects
Every Kotlin class is final by default — the opposite of Java
A Kotlin class cannot be subclassed unless it is explicitly marked <code>open</code> — the exact opposite of Java's default. This is a deliberate design decision favoring composition and explicit intent over accidental, unplanned inheritance hierarchies.
Class declarations, primary and secondary constructors, properties with custom getters/setters, and the open keyword.
Interfaces
Default method bodies from day one — a feature Java didn't get until version 8
Kotlin interfaces have supported default method implementations since the language's very first release, years before Java added the same capability in Java 8. This let Kotlin interfaces carry real shared behavior from the start, not just method signatures.
Interface declarations, default method bodies, properties in interfaces, and resolving diamond conflicts between interfaces.
Object Declarations
No static keyword at all — companion object is Kotlin's real, different answer
Kotlin has no <code>static</code> keyword whatsoever. A <code>companion object</code> — a real, singleton object tied to its class — is the language's actual, more principled answer to the class-level members Java's <code>static</code> provided, and <code>object</code> declarations cover the general singleton pattern beyond that.
object declarations for singletons, companion objects and their relationship to a class, and anonymous objects.
Null Safety and Kotlin's Type System — Its Defining Discipline
This is the stage that makes Kotlin, Kotlin: a type system that makes null a compile-time fact instead of a runtime surprise, sealed hierarchies that make that safety exhaustive, and the data-modeling and composition tools built on top. Six lessons, the heart of the course.
Null Safety and Types
Nullable types, the safe call, the Elvis operator, and smart casts
Kotlin's type system distinguishes <code>String</code> from <code>String?</code> at compile time, so the compiler — not a runtime crash — is what catches a missed null case. The safe call <code>?.</code>, the Elvis operator <code>?:</code>, and smart casts are the everyday tools built on top of that one distinction, with <code>!!</code> as the deliberately loud escape hatch.
Nullable types (String?), the safe call ?., the Elvis operator ?:, the non-null assertion !!, and smart casts after a null check.
Sealed Classes
Every possible subtype restricted to one file — so when needs no else
A <code>sealed</code> class or interface restricts every one of its possible subtypes to living in the same file, which is exactly what lets the compiler verify a <code>when</code> expression over it has handled every case — no <code>else</code> branch required, and a new subtype added later breaks the build everywhere it should.
sealed class and sealed interface declarations, exhaustive when expressions over them, and modeling state with sealed hierarchies.
Data Classes
One keyword generates equals, hashCode, toString, copy, and destructuring
Declaring a class as <code>data class</code> generates <code>equals</code>, <code>hashCode</code>, <code>toString</code>, a <code>copy</code> function for immutable updates, and destructuring components — all derived automatically from the primary constructor's properties alone, eliminating a whole category of Java boilerplate in one keyword.
data class syntax, generated equals/hashCode/toString/copy, destructuring declarations, and data class limitations (inheritance, var properties).
Generics
out and in declared once at the class — no repeating extends/super at every call site
Kotlin lets you declare a generic type parameter's variance — <code>out</code> for producer-only, <code>in</code> for consumer-only — once, at the class declaration itself, replacing Java's need to repeat <code>? extends</code> or <code>? super</code> at every single call site that uses the type.
Generic classes and functions, declaration-site variance with out/in, type projections, and reified type parameters on inline functions.
Delegation
The by keyword implements an entire interface by forwarding to another object
The <code>by</code> keyword lets a class implement an interface by forwarding every call to another object that already implements it — real composition, with almost none of the manual forwarding boilerplate that pattern would otherwise require. Property delegation (<code>by lazy</code> and friends) applies the same idea to individual properties.
Class delegation with by, property delegation (by lazy, by Delegates.observable), and writing a custom property delegate.
Extension Functions
Add a method to String or any class — no inheritance, no modifying the source
An extension function lets you add what looks like a genuine new method to <code>String</code> or any existing class — including ones you don't own the source of — without inheritance and without touching the original class at all. It's resolved statically, which is the one real limitation worth understanding.
Extension function syntax, extension properties, static resolution (no real overriding), and idiomatic uses in the standard library.
Functional Style and Collections
Lambdas as first-class values, the five scope functions everyone eventually reaches for, and how Kotlin models a collection's read-only view versus its actual mutability. Six lessons.
Lambdas and Higher-Order Functions
A trailing lambda can move outside the parentheses entirely
When a lambda is a function's last parameter, Kotlin lets it move outside the parentheses entirely — the exact syntax trick behind every readable DSL-style Kotlin API, including the standard library's own scope functions and Gradle's Kotlin DSL.
Lambda syntax, function types, higher-order functions taking/returning functions, and trailing-lambda syntax.
Scope Functions
let, run, with, apply, also — differing in exactly two ways
Kotlin's five scope functions — <code>let</code>, <code>run</code>, <code>with</code>, <code>apply</code>, <code>also</code> — differ from each other in exactly two dimensions: how you refer to the receiver inside the block (<code>it</code> vs. <code>this</code>), and what the whole expression returns (the receiver itself vs. the lambda's result). Once those two axes click, choosing between them stops being guesswork.
let, run, with, apply, and also, compared on receiver access (it/this) and return value, with idiomatic use cases for each.
Kotlin DSLs
Lambdas with a receiver are the exact mechanism behind build.gradle.kts
A lambda with a receiver — where the lambda body can call the receiver's members directly with no qualifier — is the precise language feature behind Gradle's own <code>build.gradle.kts</code> readable syntax, and the same technique is available to build your own internal DSLs.
Function types with a receiver, building a type-safe builder DSL, and why this is what makes Gradle's Kotlin DSL possible.
Collections
List isn't necessarily immutable — it's a read-only VIEW
Kotlin's <code>List</code> interface is read-only, not necessarily immutable: the same underlying data might still be mutated through a different reference that holds it as a <code>MutableList</code>, so "read-only" and "immutable" are genuinely different guarantees worth telling apart.
List/MutableList, Set/MutableSet, Map/MutableMap, the read-only-view distinction, and common collection operations (map, filter, fold).
Exception Handling
No checked exceptions at all — a deliberate rejection of a Java design
Kotlin has no checked exceptions whatsoever, a deliberate rejection of a Java design JetBrains considered, in practice, a failed experiment — one that pushed developers toward empty catch blocks and blanket <code>throws Exception</code> declarations more often than genuinely correct error handling.
try/catch/finally, try as an expression, the runCatching helper, and why Kotlin has no throws declarations to satisfy.
Annotations
A property compiles to several JVM elements — use-site targets pick which one
A single Kotlin property can compile down to several distinct underlying JVM elements — a field, a getter, a setter, a backing field — and an annotation's use-site target (<code>@field:</code>, <code>@get:</code>, and so on) is how you tell the annotation processor exactly which one to attach to.
Annotation declarations, built-in annotations (@JvmStatic, @Deprecated), use-site targets, and how they interact with Java interop.
Coroutines and Cross-Platform Kotlin
Asynchronous code without callback pyramids, structured concurrency's real guarantee, working alongside existing Java code, and how far a single Kotlin codebase can actually reach across platforms. Five lessons.
Coroutines and Flow
suspend functions, launch, async/await, and Flow for reactive streams
Coroutines let you write sequential-looking code for genuinely asynchronous work — <code>suspend</code> functions, <code>launch</code> for fire-and-forget work, <code>async</code>/<code>await</code> for a result you need back — and <code>Flow</code> extends the same idea to a stream of values arriving over time rather than a single result.
suspend functions, launch and async/await, CoroutineScope and CoroutineContext, and an introduction to Kotlin Flow.
Coroutines Deep Dive
A parent coroutine cannot complete until every child it launched has finished
Structured concurrency is coroutines' real safety guarantee: a parent coroutine genuinely cannot complete until every child coroutine it launched has itself finished, which is exactly what prevents orphaned background work from silently outliving the scope that was supposed to own it.
Structured concurrency guarantees, coroutine cancellation and cooperative checks, exception handling across coroutines, and Dispatchers.
Kotlin and Java Interop
Platform types are Kotlin's honest acknowledgment it can't verify Java's nullability
Calling Java code from Kotlin opens a genuine hole in null-safety, since Java declares no nullability information the Kotlin compiler can check. Platform types (shown as <code>String!</code>) are Kotlin's honest, visible acknowledgment of exactly that gap, rather than a false promise of safety Kotlin can't actually back up.
Calling Java from Kotlin and vice versa, platform types, @JvmStatic/@JvmOverloads for cleaner Java-facing APIs, and interop gotchas.
Kotlin Multiplatform
expect declares what every platform must provide — actual fills it in
Kotlin Multiplatform's <code>expect</code>/<code>actual</code> mechanism lets shared code declare what every target platform must provide, while each platform's own module supplies its own real implementation — sharing business logic across Android, iOS, and other targets without sharing what genuinely has to differ per platform.
expect/actual declarations, common vs. platform-specific source sets, and what Kotlin Multiplatform is and isn't good for sharing.
Kotlin/Native
No JVM, no bytecode — compiles straight to a native binary via LLVM
Kotlin/Native compiles Kotlin code directly to a native binary through LLVM, with no JVM and no bytecode involved at all — which is precisely why it exists: it's what lets Kotlin Multiplatform actually target iOS, where there is no JVM to run bytecode on in the first place.
Kotlin/Native's LLVM-based compilation model, memory management differences from the JVM, and its role in targeting iOS.
Tooling, Testing and Modern Kotlin
What surrounds the code: build tools, testing frameworks, Compose's declarative UI model, and a final lesson tying the standard library's idioms together. Five lessons to close out the course.
Build Tools
Gradle's Kotlin DSL gets full IDE autocomplete on your build file itself
Gradle's Kotlin DSL (<code>build.gradle.kts</code>) gets full IDE autocomplete and compile-time type checking directly on your build script — something the older Groovy DSL, being dynamically typed, could never genuinely offer.
build.gradle.kts structure, dependency declarations, common plugins, and the Kotlin DSL's IDE-support advantage over Groovy.
Testing
kotlin.test wraps JUnit — Kotest goes further with its own expressive DSL
<code>kotlin.test</code> is a thin, Kotlin-friendly, multiplatform-compatible wrapper directly over JUnit's assertions, while Kotest goes considerably further with a genuinely different, more expressive testing DSL of its own — worth knowing both, since real Kotlin codebases use either.
kotlin.test annotations and assertions, Kotest's DSL style, and multiplatform testing considerations.
Jetpack Compose Basics
No XML layouts — Compose describes UI as a function of state
Jetpack Compose replaces Android's traditional XML layouts and <code>findViewById</code> calls entirely: a composable function describes the UI as a direct function of the current state, and Compose automatically re-runs that function whenever the state it reads actually changes.
@Composable functions, state with remember/mutableStateOf, recomposition, and basic layout composables (Column, Row, Box).
Standard Library Deep Dive
map and filter are eager — each step builds a full new list
Kotlin's everyday collection operations like <code>map</code> and <code>filter</code> are eager by default — each step in a chain builds a complete new list immediately, even when you only actually need the first matching result — which is exactly the case <code>sequences</code> exist to handle lazily instead.
Eager vs. lazy evaluation, Sequence and its lazy chain semantics, and choosing between a List chain and a Sequence chain.
Kotlin Idioms, Tied Together
Data classes, extension functions, scope functions, collections and destructuring together
This lesson is the course's synthesis and capstone: data classes, extension functions, the five scope functions, the collections API, and destructuring declarations — the individual features from across this entire course, seen together as the actual idiomatic style real Kotlin codebases are written in, not just a list of language features in isolation.
Data classes, extension functions, scope functions (let/run/apply/also/with), the collections API, and destructuring declarations, together.
Concepts That Cross Languages
These pages explain ideas that are not specific to Kotlin — they apply to every language you will ever learn. Read them once and the next language costs you far less effort. They pair well with the lessons above rather than replacing them.
Build Real Things — 10 Projects
Reading is not enough. These 10 projects — five basic, five medium — are chosen because each one exercises something specific you learned above, and because they are genuinely idiomatic Kotlin rather than generic exercises. Each link below opens the project brief; the Kotlin implementation walkthrough is in progress.
readLine() input parsing, a guess loop, and kotlin.random.Random for the secret number.
A when expression for the conversion direction, and string templates for formatted output.
Reading a file with File(...).readText(), and a mutableMapOf<String,Int> for a frequency table.
A data class for the task shape, and hand-rolled line-based persistence.
A sealed class or enum for character-set options, and SecureRandom via Java interop for real unpredictability.
java.net.http's HttpClient via Java interop, and Kotlin's regex support for link extraction.
com.sun.net.httpserver's built-in HttpServer via interop, with sealed classes for typed responses.
A sealed class for subcommands, and an exhaustive when expression the compiler checks for completeness.
A thread-safe ConcurrentHashMap-backed store, and coroutines serving the HTTP handler.
Sequences for parsing and aggregating semi-structured lines without building intermediate lists.
Practice & Experimentation
Ongoing, not a final step. Use these throughout the course — try every snippet you read, and run anything you are unsure about rather than assuming.
Run Kotlin code without installing anything, via the official Kotlin Playground.
Copy-ready idiomatic patterns to keep beside you while you build.
All 30 topic pages in one index, for looking things up later.