Kotlin Cheatsheet - Kotlin Syntax & Coroutine Reference
This reference is for Kotlin developers — on Android, a JVM backend, or experimenting with scripts — and covers the five things you touch constantly: compiling and running with kotlinc/Gradle, null-safety idioms (safe-call, elvis, the non-null assertion), collection operators like filter/map/groupBy, and coroutines for structured concurrency. Unlike a generic Kotlin grammar dump, entries are grouped by the problem each snippet solves, from "avoid the NPE" to "run I/O off the main thread". After reading, you should be able to write null-safe chains without !! except where intended, transform collections fluently, and launch coroutines scoped so cancellation propagates correctly.
Typical Use Case
For developers writing server-side, Android or multiplatform Kotlin who are worn down by null safety and collection APIs. Four blocks sit side by side: Gradle and kotlinc for reproducible builds, safe-call and elvis operators for null handling, a declarative collection chain (filter, map, flatMap, groupBy, partition) for data work, and launch versus async plus withContext for concurrency. data class, sealed class and scope functions handle object modelling.
Compile & Run 5
kotlinc main.kt -include-runtime -d main.jarjava -jar main.jarkotlinc -script script.ktskotlinc-jvmkotlin main.ktGradle Build 5
./gradlew build./gradlew run./gradlew test./gradlew clean./gradlew dependenciesNull Safety 5
var name: String? = nullname?.lengthname ?: "default"name!!.lengthval len = name?.length ?: 0Collections 10
list.filter { it > 5 }list.map { it * 2 }list.flatMap { it.split(",") }list.groupBy { it.category }list.sortedBy { it.age }list.distinct()list.take(5)list.drop(3)list.firstOrNull { it.id == 1 }list.partition { it > 0 }Coroutines 6
runBlocking { ... }launch { ... }async { ... }delay(1000)withContext(Dispatchers.IO) { ... }coroutineScope { ... }Classes & Objects 8
data class User(val name: String, val age: Int)sealed class Resultobject Singleton { val x = 1 }companion object { val MAX = 100 }class User(val name: String)open class Animal; class Dog : Animal()interface Drawable { fun draw() }enum class Color { RED, GREEN, BLUE }Scope Functions & Delegation 8
obj.let { it.foo() }obj.apply { this.x = 1 }obj.also { it.log() }obj.run { compute() }with(obj) { foo() }val lazyVal by lazy { init() }var x by Delegates.observable(0) { _, old, new -> }val v by mapParameter matrix
| Parameter | Effect | Example |
|---|---|---|
kotlinc main.kt -include-runtime -d main.jar | Compile into an executable jar with the runtime bundled | kotlinc main.kt -include-runtime -d main.jar |
java -jar main.jar | Run the compiled jar | java -jar main.jar |
kotlinc -script script.kts | Run a Kotlin script directly | kotlinc -script demo.kts |
kotlin main.kt | Run a source file directly without compiling | kotlin main.kt |
kotlinc-jvm | Enter the REPL to try code | kotlinc-jvm |
./gradlew build | Full build: compile + test + package | ./gradlew build |
./gradlew run | Run the main class configured with the application plugin | ./gradlew run |
./gradlew test | Run the test suite; a failure fails the build | ./gradlew test |
./gradlew clean | Clear the build directory artifacts | ./gradlew clean |
./gradlew dependencies | Print the dependency tree and resolved versions | ./gradlew dependencies |
String? | Declare a nullable type whose assignment can be null | var name: String? = null |
name?.length | Safe call; the whole expression returns null when the left side is null | val n = name?.length |
name ?: "default" | Elvis fallback; takes the right side when the left side is null | val v = name ?: "default" |
name?.length ?: 0 | Chains a safe call with a fallback: if the receiver is null the whole expression collapses to the right-hand value, so no exception is thrown and no default has to be invented afterwards. | val len = name?.length ?: 0 |
name!! | Forces a non-null value and throws NullPointerException the moment it is null. Useful to silence the compiler at a boundary you have already checked, but it trades a compile-time warning for a runtime crash. | val len = name!!.length |
filter { } | Keeps only the elements that satisfy the predicate and returns a brand new list, leaving the source untouched. | list.filter { it > 5 } |
map { } | Applies the lambda to every element and collects the results. The standard move for reshaping a DTO into a view model or an entity into a UI row. | list.map { it * 2 } |
flatMap { } | Transforms each element into a collection and concatenates all of them into one flat list. This is how a one-to-many relation gets expanded into a single sequence. | list.flatMap { it.split(",") } |
groupBy { } | Computes a key per element and returns a map from that key to the matching elements, giving grouped access in a single pass. | list.groupBy { it.category } |
sortedBy { } | Returns a new list ordered by the key the lambda extracts, ascending; the original order of equal keys is preserved. | list.sortedBy { it.age } |
distinct() / take(n) / drop(n) | Three list reducers in one row: distinct drops duplicate entries, take(n) keeps the first n, drop(n) discards the first n and keeps the tail. | list.take(5) |
firstOrNull { } | Returns the first element matching the predicate, or null when nothing matches, so no out-of-bounds exception can escape. | list.firstOrNull { it.id == 1 } |
partition { } | Splits the list in one traversal into a Pair of matching and non-matching elements, which is cheaper and clearer than two filter calls. | list.partition { it > 0 } |
launch { } | Starts a coroutine whose work produces no value and hands back a Job. The result is only meaningful for cancellation and joining, not for a return value. | launch { save() } |
async { } | Starts a coroutine that yields a value and hands back a Deferred, which you await to obtain that value. Pair it with runBlocking or coroutineScope so failures cannot be lost. | val d = async { load() } |
runBlocking { } | Runs the coroutine body on the current thread and blocks until it finishes. Perfect for tests and scripts, wrong for request handlers where it would stall the event loop. | runBlocking { runApp() } |
withContext(Dispatchers.IO) { } | Moves the block onto the IO dispatcher, which is where blocking calls such as file or socket reads belong. Remember to hop back to the main dispatcher before touching the UI. | withContext(Dispatchers.IO) { readFile() } |
delay(1000) | Suspends the current coroutine for the given number of milliseconds without occupying a thread, unlike Thread.sleep which blocks the carrier thread. | delay(1000) |
coroutineScope { } | Opens a structured concurrency scope: every coroutine launched inside must finish before the block returns, and any failure cancels the siblings. | coroutineScope { launch { } } |
data class | Declares a value holder whose equals, hashCode, toString and copy are generated for you, which makes it the default choice for anything passed across layers. | data class User(val name: String, val age: Int) |
sealed class | Restricts subclassing to the same file, so a when expression over the sealed hierarchy is checked by the compiler and can be exhaustive with no else branch. | sealed class Result |
object / companion object | object declares a singleton instance with one global identity; companion object holds class-level statics and factory methods reachable without an instance. | companion object { val MAX = 100 } |
apply { } | Calls the lambda with the receiver as this and returns the receiver itself, so you can chain a block of initialisation onto a freshly built object. | obj.apply { this.x = 1 } |
let { } | Calls the lambda with the receiver bound to it and returns the lambda result. The idiomatic null-safe transform, since a null receiver short-circuits to null. | obj.let { it.foo() } |
also { } | Calls the lambda with the receiver bound to it and returns the receiver, which lets you log, validate or timestamp an object mid-pipeline without breaking the chain. | obj.also { it.log() } |
run { } / with(obj) { } | run executes the block with the receiver as this but yields the block result; with(obj) only rebinds this and still yields the block result. Neither returns the receiver. | obj.run { compute() } |
by lazy { } | Property delegate that defers the initialiser until the first read, then caches the result. Handy for expensive setup that may never be needed on a given code path. | val lazyVal by lazy { init() } |
Delegates.observable | Property delegate that fires a callback on every assignment, receiving the property, the old value and the new one, which is how change observation is wired without extra plumbing. | var x by Delegates.observable(0) { _, old, n -> } |
map delegate | Property delegate that looks the value up in a Map by key at read time. A missing key throws instead of returning null, so guard the lookup if the key is optional. | val v by map |
Common pitfalls
SymptomExecution throws a NullPointerException somewhere, with a stack pointing to a line that looks fine
CauseThat line uses the !! non-null assertion, but the value is actually null at runtime
FixReplace !! with ?. plus a ?: fallback so null takes an explicit degrade path instead of crashing
SymptomA chained collection operation runs halfway, then downstream gets null without any error
CauseSome intermediate step used ?. safe calls, so null was silently passed downstream
FixAdd a default with ?: at the end of the chain, or use ?: with an early return to short-circuit empty data as early as possible
SymptomThe API responds slowly and the UI looks frozen
CauseA blocking call was made directly on the main thread without switching to the IO context
FixWrap the blocking read/write in withContext(Dispatchers.IO) { ... }
SymptomAfter starting a coroutine you cannot get the result; the await position is always the default value
Causelaunch was used instead of async, and launch returns a Job with no result
FixWhen you need a return value, switch to async and await its Deferred within the coroutine scope
SymptomIt compiles locally but reports dependency version conflicts in CI
CauseLocally you used machine-local caches and dependencies outside this repository, so version resolution differs
FixStandardize on ./gradlew dependencies to print the resolution result for cross-checking, commit the lock file, and build only through ./gradlew build
SymptomWriting a when for a sealed class keeps demanding an else branch
CauseThe when branches do not cover all subclasses, or the check is not of a constant
FixCheck whether a subclass declaration was missed, or change the condition to a comparison against constant values so the compiler can exhaustively enumerate them
SymptomMemory usage suddenly rises after switching to flatMap
CauseEvery step of a chained collection operation produces an intermediate collection, piling up layer by layer with large data
FixReduce the chain length, switch to asSequence() for lazy processing on large collections, or do deduplication and sorting only at the end
Troubleshooting
1Verify from scratch whether the current environment can compile and run
./gradlew clean buildClear the artifacts first, then do a full build; if it fails here, look at the individual checks below—do not skip clean.
2Confirm that null-safety constructs stop the problem at compile time
val len = name?.length ?: 0This neither throws nor returns null; after replacing !!, the compiler's nullability warnings also decrease.
3Locate which transitive dependency causes a version conflict
./gradlew dependenciesIn the dependency tree output, find where the same library appears in multiple versions, then exclude as needed or constrain to a single version.
4Keep blocking IO calls off the main thread
withContext(Dispatchers.IO) { readFile(path) }If the time drops noticeably afterward, it means you really were running a blocking call on the main thread before.
5Quickly verify the return value of a coroutine branch
runBlocking { val v = async { load() }; println(v.await()) }runBlocking waits synchronously in a test or script, making it easy to observe the actual await result.
6Check that the split and grouping logic matches your intent
list.partition { it > 0 }The returned Pair holds the matching elements in first and the non-matching ones in second, so print both sides: a single element landing on the wrong half of the pair is the usual cause of a miscounted bucket.
7Package the source into a distributable artifact
kotlinc main.kt -include-runtime -d main.jarA successful run produces main.jar, and java -jar main.jar executes it immediately, which confirms the artifact is self-contained before you ship it anywhere.
Command Examples
A null-safe chain: safe call plus Elvis fallback in one expression
val name: String? = loadName()
val len = name?.length ?: 0
println(len)?. short-circuits to null when name is null, and ?: swaps that null for 0. This replaces almost every !! in practice, removing the NPE risk instead of relocating it.
Output
0
firstOrNull instead of first to avoid an exception on an empty collection
val user = list.firstOrNull { it.id == 1 }
println(user?.name ?: "anonymous")first() throws NoSuchElementException when nothing matches, yet a miss is a normal branch in paginated lookups. firstOrNull { } turns it into a nullable value that composes with ?:.
Output
anonymous
Flatten nested API payloads into a flat DTO list
val rows = response.body
.flatMap { it.items }
.map { Item(id = it.id, title = it.name) }
.groupBy { it.title.first().uppercase() }flatMap expands the one-to-many payload, map converts each entry to a view model, and groupBy buckets by any key. It is the standard list-page pipeline, and every step returns a new collection.
Output
{ A=[Item(id=1, title=Alpha)], B=[Item(id=2, title=Beta)] }Run blocking calls on the IO dispatcher with withContext
suspend fun load(): List<Item> = withContext(Dispatchers.IO) {
repository.blockingQuery()
}Calling a blocking API straight from a coroutine pins the current thread. withContext(Dispatchers.IO) moves the block to the IO pool and switches back afterwards — the standard seam for wrapping legacy blocking code.
Inspect resolved dependency versions before upgrading
./gradlew dependencies --configuration releaseRuntimeClasspathRead the resolved tree before bumping anything: a dynamic version can land on an unexpected release because of a transitive conflict. Checking first turns a mysterious broken build into a one-line diff.
Common Pitfalls
!!fails at runtime on a path the compiler already allowed: if the upstream value really is null it is a production NPE. Anywhere?.or?:works, do not leave!!.- Stale artifacts under
build/can poison incremental compilation. When logic changes are not taking effect, run./gradlew cleanbefore blaming caches or reinstalling dependencies. - In Kotlin
==compares references, not content (same rule as Java), so usedistinct()or data-classequalsfor de-duplication or duplicates slip through. - Initialization deferred by
lateinitorby lazythrows only on first access, so the stack trace points far from the real fault — especially hard to pin down inside coroutines. - Always use
./gradlew, never a globalgradle: a mismatched local JDK or Gradle version produces the classic "works for everyone else" build failure. runBlockingblocks the current thread until the coroutine finishes and deadlocks if called from asuspendfunction or inside an existing scope. Restrict it to tests andmain.
Tips
- Kotlin null safety is a compile-time check - avoid !! when you can.
- Coroutine launch returns a Job; async returns a Deferred you can await.
- Collection chaining is elegant, but watch intermediate-collection memory on large data.
- data class auto-generates copy/equals/hashCode/toString - ideal for data models.
- Scope functions: use apply to init, let/let it to transform, also for side-effects.
- sealed class + when lets the compiler verify exhaustiveness - no else needed.
FAQ
?. , ?: and !! all "handle null" — which one should I actually reach for?
The priority is clear. ?. is the default: name?.length evaluates to null for the whole expression instead of throwing. ?: supplies a fallback, and val len = name?.length ?: 0 chains the two together — the most common null-safe idiom in a call chain. !! is a lie told to the compiler: it asserts non-null and throws NPE if that turns out to be false, converting a compile-time check into a runtime crash. Justify !! only when a non-null check happens on the immediately preceding line or the value is guaranteed by an immutable construction; everywhere else ?. or ?: does the job.
What is the real difference between launch and async, and when is async mandatory?
Only the return value differs. launch gives back a Job, meaning "this coroutine started and finished" — no result. async gives back a Deferred, which you can await to obtain the value. The rule is plain: use launch when nobody consumes the coroutine body's result; use async when you need a return value and genuinely want to run several concurrently and collect them. Both start on the current context by default, so wrap blocking work in withContext(Dispatchers.IO) { ... }. In tests, use runBlocking { ... } to let the coroutine finish before asserting rather than really sleeping with delay(1000).
let, apply, also and run all pass the object into a lambda — how do I tell them apart?
Two dimensions decide it: whether the receiver is it or this, and whether you get the lambda result back or the object back. let { it.foo() } and with(obj) { foo() } both return the lambda result, with let using it for a chained transformation. apply { this.x = 1 } returns the object itself and its receiver is this, which makes it the right pick for initialising a batch of properties. also { it.log() } also returns the original object but binds it, so you can slot in a side effect and keep chaining. run { compute() } is this-receiver plus returned result, handy for calling several members on a non-null object. Mnemonic: apply to initialise, let to transform, also for side effects.
first() versus firstOrNull() looks like a one-word difference — how should I pick when reading from a list?
first() throws NoSuchElementException when nothing matches, while firstOrNull { it.id == 1 } hands back null, so the second is the safe choice whenever the data may be incomplete — and it composes naturally with a following null check. Alongside it sit the one-pass collection operations: groupBy { it.category } buckets by any key into a Map, partition { it > 0 } splits in a single pass and returns a Pair (first = matching, second = not), distinct() deduplicates by equals, take(5) with drop(3) implements paging, and sortedBy { it.age } returns ascending order that reversed can flip. All of these return new collections and leave the original untouched, so watch the memory cost of intermediates on large data.
When should I reach for sealed class, enum class, or data class?
A sealed class Result models a restricted hierarchy: every subclass lives in the same file, and when used with a when expression the compiler checks that branches are exhaustive, so you need no else. That makes it ideal for mutually exclusive states like success, failure, and loading. enum class Color { RED, GREEN, BLUE } is a fixed set of constants and comes with its own value list. data class User(val name: String, val age: Int) fits pure data models — the compiler generates equals, hashCode, toString and copy(), so a single changed field is expressed as val b = a.copy(age = 30). Remember to mark a base class open if it should be subclassed.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us