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.

Languages·47 commands·Last updated 2026-07-21
kotlingradlejvm

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.jar
Compile to an executable jar
java -jar main.jar
Run the compiled jar
kotlinc -script script.kts
Run a Kotlin script
kotlinc-jvm
Enter the REPL
kotlin main.kt
Run directly (no compile)

Gradle Build 5

./gradlew build
Build the project
./gradlew run
Run the main class
./gradlew test
Run tests
./gradlew clean
Clean the build dir
./gradlew dependencies
Show dependencies

Null Safety 5

var name: String? = null
Nullable type declaration
name?.length
Safe call (null -> null)
name ?: "default"
Elvis operator (default on null)
name!!.length
Non-null assertion (throws on null)
val len = name?.length ?: 0
Combine the two

Collections 10

list.filter { it > 5 }
Filter
list.map { it * 2 }
Map
list.flatMap { it.split(",") }
FlatMap
list.groupBy { it.category }
GroupBy
list.sortedBy { it.age }
SortedBy
list.distinct()
Distinct
list.take(5)
Take first 5
list.drop(3)
Drop first 3
list.firstOrNull { it.id == 1 }
Find first match
list.partition { it > 0 }
Partition by predicate

Coroutines 6

runBlocking { ... }
Blocking coroutine (tests)
launch { ... }
Launch (no return value)
async { ... }
Async (returns Deferred)
delay(1000)
Non-blocking delay
withContext(Dispatchers.IO) { ... }
Switch context
coroutineScope { ... }
Create a coroutine scope

Classes & Objects 8

data class User(val name: String, val age: Int)
Data class (auto equals/hashCode/toString/copy)
sealed class Result
Sealed class (exhaustive when)
object Singleton { val x = 1 }
Singleton object
companion object { val MAX = 100 }
Companion object (class-level)
class User(val name: String)
Primary ctor declares property
open class Animal; class Dog : Animal()
Inheritance (parent must be open)
interface Drawable { fun draw() }
Interface definition
enum class Color { RED, GREEN, BLUE }
Enum class

Scope Functions & Delegation 8

obj.let { it.foo() }
let: it receiver, returns lambda result
obj.apply { this.x = 1 }
apply: this receiver, returns the object (init)
obj.also { it.log() }
also: it receiver, returns object (side-effects)
obj.run { compute() }
run: this receiver, returns lambda result
with(obj) { foo() }
with: this receiver, returns lambda result
val lazyVal by lazy { init() }
Lazy init delegation (computed on first access)
var x by Delegates.observable(0) { _, old, new -> }
Observable property delegation
val v by map
Map delegation (keyed access)

Parameter matrix

ParameterEffectExample
kotlinc main.kt -include-runtime -d main.jarCompile into an executable jar with the runtime bundledkotlinc main.kt -include-runtime -d main.jar
java -jar main.jarRun the compiled jarjava -jar main.jar
kotlinc -script script.ktsRun a Kotlin script directlykotlinc -script demo.kts
kotlin main.ktRun a source file directly without compilingkotlin main.kt
kotlinc-jvmEnter the REPL to try codekotlinc-jvm
./gradlew buildFull build: compile + test + package./gradlew build
./gradlew runRun the main class configured with the application plugin./gradlew run
./gradlew testRun the test suite; a failure fails the build./gradlew test
./gradlew cleanClear the build directory artifacts./gradlew clean
./gradlew dependenciesPrint the dependency tree and resolved versions./gradlew dependencies
String?Declare a nullable type whose assignment can be nullvar name: String? = null
name?.lengthSafe call; the whole expression returns null when the left side is nullval n = name?.length
name ?: "default"Elvis fallback; takes the right side when the left side is nullval v = name ?: "default"
name?.length ?: 0Chains 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 classDeclares 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 classRestricts 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 objectobject 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.observableProperty 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 delegateProperty 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

  1. 1Verify from scratch whether the current environment can compile and run

    ./gradlew clean build

    Clear the artifacts first, then do a full build; if it fails here, look at the individual checks below—do not skip clean.

  2. 2Confirm that null-safety constructs stop the problem at compile time

    val len = name?.length ?: 0

    This neither throws nor returns null; after replacing !!, the compiler's nullability warnings also decrease.

  3. 3Locate which transitive dependency causes a version conflict

    ./gradlew dependencies

    In the dependency tree output, find where the same library appears in multiple versions, then exclude as needed or constrain to a single version.

  4. 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.

  5. 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.

  6. 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.

  7. 7Package the source into a distributable artifact

    kotlinc main.kt -include-runtime -d main.jar

    A 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 releaseRuntimeClasspath

Read 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 clean before blaming caches or reinstalling dependencies.
  • In Kotlin == compares references, not content (same rule as Java), so use distinct() or data-class equals for de-duplication or duplicates slip through.
  • Initialization deferred by lateinit or by lazy throws 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 global gradle: a mismatched local JDK or Gradle version produces the classic "works for everyone else" build failure.
  • runBlocking blocks the current thread until the coroutine finishes and deadlocks if called from a suspend function or inside an existing scope. Restrict it to tests and main.

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