Kotlin cheat sheet

A scannable Kotlin reference: 27 short snippets across 13 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
Syntax and null safetyIn Kotlin String and String? are different types. The compiler refuses to call anything on a nullable value until youlesson
Classes, data classes and extensionsA data class must have at least one constructor parameter and generates equals, hashCode, toString, copy and componentlesson
Coroutines and collectionsThe OrNull family (firstOrNull, singleOrNull, maxOrNull) returns null instead of throwing, which fits Kotlin'slesson
Setting up Kotlin: Gradle, the K2 compiler and project layoutThe package declaration must match the directory from src/main/kotlin downwards. Kotlin does not enforce it the waylesson
Generics, variance and delegationType parameters and bounds, declaration-site and use-site variance, star projections, reified inline generics, andlesson
Functional Kotlin: lambdas, inline functions and DSLsFunction types and receivers, higher-order functions, inline and noinline, value classes, and building a smalllesson
Channels, shared state and advanced concurrencyChannel and produce, select, Mutex and atomics, SharingStarted strategies, exception handling, and deterministiclesson
Exceptions, Result and error-handling patternstry as an expression, runCatching and Result, sealed error hierarchies, require and check, and validating at thelesson
Testing Kotlin with JUnit 5, Kotest and MockKStructure tests and assertions, parameterise cases, use property-based testing, mock with MockK, and wire coverage andlesson
JSON, serialization and HTTP clientskotlinx.serialization with custom serializers, Ktor Client and Retrofit, timeouts, retries and mapping transport errorslesson
Java interop, JVM tooling and build performanceKotlin compiles records to a normal class from the language's point of view. Use @JvmRecord on a Kotlin data class onlylesson
Kotlin Multiplatform: expect, actual and shared modulesPut as much as possible in commonMain. Every platform-specific API you add there forces an expect declaration and atlesson
Building a REST service with KtorProject setup, routing, content negotiation, plugins, authentication, Exposed database access, configuration, logginglesson

Quick snippets

Syntax and null safety

Values, functions, control flow

val name: String = "Ada"      // immutable reference
var count = 0                  // mutable, type inferred as Int

fun greet(who: String, greeting: String = "Hello"): String =
    greeting + ", " + who

// if and when are expressions, so they produce values
val label = if (count == 0) "empty" else "n items"

val grade = when (count) {
    0 -> "F"
    in 1..49 -> "D"

… 6 more lines in the full lesson.

Nullability is in the type

data class User(val email: String, val phone: String?)

fun contact(user: User?): String {
    val email = user?.email ?: return "unknown"
    val phone = user.phone?.takeIf { it.isNotBlank() } ?: "no phone"
    return email + " / " + phone
}

// let runs the block only for a non-null value
user?.let { sendWelcome(it.email) }

// smart cast after an explicit check

… 1 more lines in the full lesson.

Full lesson: Syntax and null safety →

Classes, data classes and extensions

Extension functions and scope functions

// add a function to a type you do not own, without inheritance
fun String.titleCase(): String =
    split(' ').joinToString(" ") { it.lowercase().replaceFirstChar(Char::uppercaseChar) }

fun <T> List<T>.secondOrNull(): T? = if (size > 1) this[1] else null

"hello world".titleCase()          // "Hello World"
listOf(1, 2, 3).secondOrNull()     // 2

// inside the extension, this is the receiver
fun Account.describe(): String = owner + " has " + balance

Classes and interfaces

interface Notifier {
    fun send(message: String)
    fun label(): String = this::class.simpleName ?: "notifier"   // default body
}

open class Account(val owner: String, opening: Long = 0) {
    var balance: Long = opening
        private set                                   // readable, not writable outside

    init {
        require(opening >= 0) { "opening must not be negative" }
    }

… 12 more lines in the full lesson.

Data classes and sealed types

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

val p = Point(1, 2)
val q = p.copy(y = 5)              // Point(x=1, y=5)
val (x, y) = p                     // destructuring
println(p)                         // Point(x=1, y=2) - generated toString

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

… 4 more lines in the full lesson.

Full lesson: Classes, data classes and extensions →

Coroutines and collections

Collections and the operations on them

data class Sale(val region: String, val amount: Int)

val sales = listOf(
    Sale("EU", 120), Sale("US", 80), Sale("EU", 45), Sale("APAC", 200),
)

val big = sales.filter { it.amount >= 100 }          // keeps order
val regions = sales.map { it.region }.distinct()
val byRegion = sales.groupBy { it.region }           // Map<String, List<Sale>>
val totals = sales.groupBy { it.region }
    .mapValues { (_, rows) -> rows.sumOf { it.amount } }
val ranked = sales.sortedByDescending { it.amount }

… 8 more lines in the full lesson.

Coroutines

suspend fun loadUser(id: Long): User = withContext(Dispatchers.IO) {
    api.fetch(id)                       // switches thread, then switches back
}

fun main() = runBlocking {
    val one = launch { println(loadUser(1).email) }   // fire and forget
    val two = async { loadUser(2).email }             // returns a Deferred
    println(two.await())
    one.join()
}

// parallel work with automatic cancellation

… 3 more lines in the full lesson.

Flow for streams

fun ticks(): Flow<Int> = flow {
    var i = 0
    while (true) {
        emit(i++)
        delay(1000)
    }
}

val viewModelScopeJob = CoroutineScope(Dispatchers.Main).launch {
    ticks()
        .filter { it % 2 == 0 }
        .map { it * 10 }

… 6 more lines in the full lesson.

Full lesson: Coroutines and collections →

Setting up Kotlin: Gradle, the K2 compiler and project layout

A minimal Gradle build

gradle wrapper --gradle-version 8.11     # commit the wrapper, not the binary

./gradlew run
./gradlew test
./gradlew build --scan                   # a build scan explains where time goes
./gradlew installDist                    # runnable script plus jars in build/install

Splitting into modules

// settings.gradle.kts
rootProject.name = "service"
include(":core", ":app", ":adapters:postgres")

// app/build.gradle.kts
dependencies {
    implementation(project(":core"))
    runtimeOnly(project(":adapters:postgres"))
}

Full lesson: Setting up Kotlin: Gradle, the K2 compiler and project layout →

Generics, variance and delegation

Variance explained by use

// out: a producer. Read values out of it, never put them in.
interface Source<out T> {
    fun next(): T
}

// in: a consumer. Put values in, never read them out.
interface Sink<in T> {
    fun accept(value: T)
}

class NumberSource : Source<Number> {
    override fun next(): Number = 42

… 12 more lines in the full lesson.

Bounds, projections and reified

fun <T : Comparable<T>> maxOfList(items: List<T>): T {
    require(items.isNotEmpty()) { "list must not be empty" }
    return items.reduce { a, b -> if (a > b) a else b }
}

// multiple bounds need a where clause
fun <T> merge(a: T, b: T): T where T : CharSequence, T : Appendable = a

// star projection: the element type is unknown, so you can only read Any?
fun size(items: List<*>) = items.size

// reified keeps the type at runtime, which requires inline

… 8 more lines in the full lesson.

Full lesson: Generics, variance and delegation →

Functional Kotlin: lambdas, inline functions and DSLs

Value classes and operator conventions

@JvmInline
value class UserId(val raw: String)

@JvmInline
value class Money(val pence: Long) {
    operator fun plus(other: Money) = Money(pence + other.pence)
    operator fun times(factor: Int) = Money(pence * factor)
    override fun toString() = "GBP " + pence / 100.0
}

fun charge(id: UserId, amount: Money) = println("charging " + id.raw + " " + amount)

… 3 more lines in the full lesson.

A small type-safe DSL

@DslMarker
annotation class RouteDsl

@RouteDsl
class RouteBuilder(private val prefix: String) {
    private val children = mutableListOf<String>()

    fun get(path: String, handler: (String) -> String) {
        children += "GET " + prefix + path + " -> " + handler(prefix + path)
    }

    fun post(path: String, handler: (String) -> String) {

… 13 more lines in the full lesson.

Full lesson: Functional Kotlin: lambdas, inline functions and DSLs →

Channels, shared state and advanced concurrency

Channels and pipelines

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

fun CoroutineScope.numbers(): ReceiveChannel<Int> = produce {
    for (i in 1..5) send(i)
}   // the channel closes automatically when the block completes

suspend fun main() = coroutineScope {
    val input = numbers()
    val doubled = produce {
        for (value in input) send(value * 2)
    }

… 11 more lines in the full lesson.

Exceptions and deterministic tests

import kotlinx.coroutines.test.*

class TickerTest {
    @Test
    fun refreshExposesTheError() = runTest {
        val repository = FakeRepository(failure = IllegalStateException("offline"))
        val ticker = Ticker(repository, backgroundScope)

        ticker.refresh()
        advanceUntilIdle()

        assertEquals("offline", ticker.state.value.error)

… 9 more lines in the full lesson.

Full lesson: Channels, shared state and advanced concurrency →

Exceptions, Result and error-handling patterns

try, runCatching and Result

// try is an expression, so it can initialise a value
val port: Int = try {
    System.getenv("PORT").toInt()
} catch (_: NumberFormatException) {
    8080
}

// runCatching turns a throwing call into a Result
fun parseAmount(raw: String): Result<Long> = runCatching {
    require(raw.isNotBlank()) { "amount must not be blank" }
    val value = raw.removePrefix("GBP").trim().toLong()
    require(value >= 0) { "amount must not be negative" }

… 12 more lines in the full lesson.

A sealed error type

sealed interface LoadError {
    data class Network(val cause: Throwable) : LoadError
    data class Http(val status: Int, val body: String?) : LoadError
    data object NotFound : LoadError
    data class Decoding(val path: String) : LoadError
}

suspend fun loadUser(id: String): Either<User> =
    try {
        val response = client.get("/users/" + id)
        when (response.status.value) {
            in 200..299 -> Either.Ok(json.decodeFromString<User>(response.body()))

… 15 more lines in the full lesson.

Checking at the boundary

// one place converts untrusted input into a trusted type
@JvmInline
value class Email private constructor(val value: String) {
    companion object {
        fun parse(raw: String): Email? {
            val trimmed = raw.trim().lowercase()
            return if (trimmed.length in 3..254 && "@" in trimmed && !trimmed.startsWith("@")) {
                Email(trimmed)
            } else {
                null
            }
        }

… 7 more lines in the full lesson.

Full lesson: Exceptions, Result and error-handling patterns →

Testing Kotlin with JUnit 5, Kotest and MockK

Kotest and property-based testing

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.list
import io.kotest.property.checkAll

class CartSpec : StringSpec({
    "an empty cart totals zero" {
        Cart().total() shouldBe 0
    }

… 13 more lines in the full lesson.

Full lesson: Testing Kotlin with JUnit 5, Kotest and MockK →

JSON, serialization and HTTP clients

Mapping transport failures

suspend fun loadArticle(id: Long): Either<Article> = try {
    Either.Ok(client.get("articles/$id").body())
} catch (e: CancellationException) {
    throw e
} catch (e: ClientRequestException) {        // 4xx
    if (e.response.status == HttpStatusCode.NotFound) Either.Err(LoadError.NotFound)
    else Either.Err(LoadError.Http(e.response.status.value, null))
} catch (e: ServerResponseException) {       // 5xx
    Either.Err(LoadError.Http(e.response.status.value, null))
} catch (e: HttpRequestTimeoutException) {
    Either.Err(LoadError.Network(e))
} catch (e: IOException) {

… 4 more lines in the full lesson.

Full lesson: JSON, serialization and HTTP clients →

Java interop, JVM tooling and build performance

Calling Java and being called from it

// platform types: Java returns String!, which is neither String nor String?
val javaName: String = legacy.getName()      // compiles, may throw at runtime
val safeName: String? = legacy.getName()     // honest about the risk
val checked = requireNotNull(legacy.getName()) { "name missing from legacy API" }

// make Java callers comfortable with the Kotlin API
class ReportService @JvmOverloads constructor(
    private val title: String,
    private val pageSize: Int = 50,
    private val includeCharts: Boolean = false,
) {
    @JvmStatic

… 11 more lines in the full lesson.

Annotation processing and build speed

// gradle.properties: the settings that actually move the needle
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.incremental=true
kotlin.incremental.useClasspathSnapshot=true

// Prefer KSP over kapt: it runs ahead of compilation and is several times faster
plugins {
    id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}

… 3 more lines in the full lesson.

Full lesson: Java interop, JVM tooling and build performance →

Kotlin Multiplatform: expect, actual and shared modules

Source sets and targets

// shared/build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.1.0"
    kotlin("plugin.serialization") version "2.1.0"
}

kotlin {
    androidTarget()
    iosX64(); iosArm64(); iosSimulatorArm64()
    jvm()

    sourceSets {

… 11 more lines in the full lesson.

expect and actual

// commonMain
expect class PlatformClock() {
    fun nowMillis(): Long
    val name: String
}

expect fun currentPlatform(): String

// androidMain
actual class PlatformClock actual constructor() {
    actual fun nowMillis(): Long = System.currentTimeMillis()
    actual val name: String = "android"

… 12 more lines in the full lesson.

What is worth sharing

// shared/src/commonMain/kotlin/com/example/domain/ArticleRepository.kt
class ArticleRepository(
    private val api: ArticleApi,
    private val cache: ArticleCache,
) {
    suspend fun recent(): List<Article> =
        runCatching { api.recent() }
            .onSuccess { cache.save(it) }
            .getOrElse { cache.load() ?: emptyList() }
}

// iOS consumes the shared module as a framework and calls into it from Swift

… 2 more lines in the full lesson.

Full lesson: Kotlin Multiplatform: expect, actual and shared modules →

Building a REST service with Ktor

Exposed and lifecycle

object Articles : LongIdTable("articles") {
    val title = varchar("title", 200)
    val publishedAt = timestamp("published_at")
    val author = varchar("author", 120).nullable()
}

class ArticleService(private val db: Database) {
    suspend fun page(page: Int, size: Int = 20): List<ArticleDto> = newSuspendedTransaction(Dispatchers.IO, db) {
        Articles.selectAll()
            .orderBy(Articles.publishedAt to SortOrder.DESC)
            .limit(size).offset(((page - 1) * size).toLong())
            .map(::toDto)

… 15 more lines in the full lesson.

Full lesson: Building a REST service with Ktor →

FAQ

Is this Kotlin cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 13 lessons of the Kotlin course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full Kotlin course — it carries the worked explanations, the edge cases and the exercises behind every line here.

Android iOS Flutter React Native Swift

Last refreshed 2026-09-27.