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
| Topic | What it covers | |
|---|---|---|
| Syntax and null safety | In Kotlin String and String? are different types. The compiler refuses to call anything on a nullable value until you | lesson |
| Classes, data classes and extensions | A data class must have at least one constructor parameter and generates equals, hashCode, toString, copy and component | lesson |
| Coroutines and collections | The OrNull family (firstOrNull, singleOrNull, maxOrNull) returns null instead of throwing, which fits Kotlin's | lesson |
| Setting up Kotlin: Gradle, the K2 compiler and project layout | The package declaration must match the directory from src/main/kotlin downwards. Kotlin does not enforce it the way | lesson |
| Generics, variance and delegation | Type parameters and bounds, declaration-site and use-site variance, star projections, reified inline generics, and | lesson |
| Functional Kotlin: lambdas, inline functions and DSLs | Function types and receivers, higher-order functions, inline and noinline, value classes, and building a small | lesson |
| Channels, shared state and advanced concurrency | Channel and produce, select, Mutex and atomics, SharingStarted strategies, exception handling, and deterministic | lesson |
| Exceptions, Result and error-handling patterns | try as an expression, runCatching and Result, sealed error hierarchies, require and check, and validating at the | lesson |
| Testing Kotlin with JUnit 5, Kotest and MockK | Structure tests and assertions, parameterise cases, use property-based testing, mock with MockK, and wire coverage and | lesson |
| JSON, serialization and HTTP clients | kotlinx.serialization with custom serializers, Ktor Client and Retrofit, timeouts, retries and mapping transport errors | lesson |
| Java interop, JVM tooling and build performance | Kotlin compiles records to a normal class from the language's point of view. Use @JvmRecord on a Kotlin data class only | lesson |
| Kotlin Multiplatform: expect, actual and shared modules | Put as much as possible in commonMain. Every platform-specific API you add there forces an expect declaration and at | lesson |
| Building a REST service with Ktor | Project setup, routing, content negotiation, plugins, authentication, Exposed database access, configuration, logging | lesson |
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?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
Android iOS Flutter React Native Swift
Last refreshed 2026-09-27.