DS DevShelfHub Projects · AI tools
Cheatsheets / Kotlin
Cheatsheet · Languages

Kotlin: Null Safety, Data Classes, Coroutines and Flows Reference Guide

By DevShelfHub

Null safety, data classes, sealed/enum, coroutines, flows, scope/extension functions, collections, JVM/Android idioms — the modern Kotlin surface.

118 items 8 min Coroutines Null-safe Data classes

Start hereQuick start · 6 you’ll reach for daily

Varsval x = 5 · var y = 1
Null-safename?.length ?: 0
Data classdata class User(val id: String)
Coroutinelaunch { delay(100); … }
Configureobj.apply { … }
Whenwhen(x) { is A -> … }

Target versions · paceVersions

Targets: kotlin ≥ 2.0 kotlinx.coroutines ≥ 1.8 JVM target 17 (recommended) gradle ≥ 8

Kotlin 2.0 ships the new K2 compiler — fully stable, ~2× faster, with better type inference. On the JVM, target Java 17 unless you have a reason to stay on 11. For Android, the Kotlin version is gated by your AGP; check kotlin-android.gradle.plugin compatibility before bumping. Coroutines and Flow are the de-facto concurrency stack; RxJava remains supported but is no longer the default.

Install · GradleSetup

bash
# Install via SDKMAN (recommended)
curl -s "https://get.sdkman.io" | bash
sdk install kotlin                  # JVM compiler + kotlin REPL
sdk install gradle

# Or via Homebrew
brew install kotlin gradle

# Run a single file
kotlin hello.kts                    # script (.kts)
kotlinc Hello.kt -include-runtime -d hello.jar && java -jar hello.jar

# New Gradle project (Kotlin DSL)
gradle init --type kotlin-application --dsl kotlin
./gradlew run

# Daily commands
./gradlew build                     # compile + test
./gradlew test --tests "MyTest"
./gradlew installDist               # build/install/bin/
./gradlew ktlintCheck detekt        # if you've added the plugins

Where things liveCommon imports

import kotlin.collections.*Implicit. List, Map, Set, MutableList…
import kotlinx.coroutines.*launch, async, runBlocking, withContext, Dispatchers.
import kotlinx.coroutines.flow.*Flow, StateFlow, SharedFlow, collect, map, filter.
import kotlinx.serialization.*@Serializable + Json.
import kotlin.time.Duration, Duration.Companion.secondsType-safe durations: 1.seconds.
import kotlin.io.path.*Path extensions: readText, exists, listDirectoryEntries.
import kotlin.system.measureTimeMillisQuick perf measurements.
import org.jetbrains.kotlin.…Compiler / plugin APIs — rare for app code.

val · var · primitivesVariables & types

Bindings

val x = 5Immutable. Default. Use whenever possible.
var y = 5Mutable.
val x: Int = 5Explicit type. Inferred otherwise.
const val PI = 3.14Compile-time constant. Top-level / companion only.
lateinit var s: StringInitialized later. Throws if read before init.
val v by lazy { compute() }Initialize on first access, then cache.

Built-in types

Int, Long, Short, ByteSigned integers. No implicit widening.
UInt, ULong, UShort, UByteUnsigned variants. Interop-aware.
Float, DoubleIEEE-754.
Boolean, Char, StringBooleans, single chars, immutable strings.
Any, Unit, NothingRoot, void-equivalent, "never returns" bottom type.
String templates: "x = $x" / "${expr}"Interpolation. Use ${} for expressions.
"""raw text"""Multi-line raw string. .trimIndent() strips margin.

The big ideaNull safety

var s: String = "hi"Non-null. s = null won’t compile.
var s: String? = nullNullable.
s?.lengthSafe call. Returns null if s is null.
s?.length ?: 0Elvis. Default when null.
s!!.lengthForce-unwrap. Throws NPE if null. Avoid.
s?.let { use(it) }Run a block only when non-null.
if (s != null) { s.length }Smart cast: s is treated as String inside the block.
requireNotNull(x) { "x missing" }Throw IllegalArgumentException with a message.
checkNotNull(x)Throw IllegalStateException instead.

Defaults · named args · lambdasFunctions

fun add(a: Int, b: Int): Int = a + bSingle-expression body. No braces needed.
fun add(a: Int, b: Int = 0): Int { … }Default parameter.
add(a = 1, b = 2)Named arguments. Order can change.
fun greet(vararg names: String)Vararg. Spread with *names when forwarding.
val sum = { a: Int, b: Int -> a + b }Lambda. Last param of type (Int, Int) -> Int.
xs.filter { it > 0 }it is the implicit single argument.
trailing lambda syntaxIf the last arg is a lambda, move it outside the parens.
inline fun …Inline at call site. Lets lambdas use return non-locally.
fun String.toSlug(): String = lowercase().replace(" ", "-")Extension function. "hi".toSlug().
infix fun Int.times(s: String): String = s.repeat(this)Infix call: 3 times "ab".

Data · sealed · enumClasses

Class basics

class Point(val x: Int, val y: Int)Primary constructor + property declaration in one.
class Foo { init { … } }Initializer block. Runs after the primary constructor.
class Foo(val name: String) { constructor() : this("x") }Secondary constructor. Must call primary.
open class Animal { open fun cry() = "…" }Classes/methods are final by default; open to allow override.
abstract class Vehicle { abstract fun go() }Abstract members. Can’t instantiate.
interface Greeter { fun hello() }Interfaces can have default implementations.
object Singleton { fun ping() = "…" }Singleton. Eagerly created.
companion object { const val MAX = 10 }Static-like members on a class.

Data, sealed, enum, value

data class User(val id: String, val name: String)Auto equals/hashCode/toString/copy/componentN.
user.copy(name = "Ada")Immutable update.
val (id, name) = userDestructuring via componentN().
sealed interface Result<out T>Closed hierarchy. Exhaustive when.
enum class Role(val label: String) { Admin("admin"), Member("member") }Enums with properties + methods.
@JvmInline value class UserId(val raw: String)Inline value class. Zero-cost wrapper.

Worked example

kotlin
// Data class — equals/hashCode/toString/copy auto-generated
data class User(
    val id: String,
    val name: String,
    val email: String? = null,        // nullable + default
)

// Sealed hierarchy — exhaustive when() with no else needed
sealed interface Result {
    data class Ok(val value: T) : Result
    data class Err(val message: String) : Result
}

// Enum with members
enum class Role(val label: String) {
    Admin("admin"), Member("member"), Guest("guest");

    val isPrivileged: Boolean get() = this == Admin
}

fun describe(r: Result): String = when (r) {
    is Result.Ok  -> "ok: ${r.value.name}"
    is Result.Err -> "failed: ${r.message}"
    // exhaustive — compiler verifies all variants are covered
}

fun main() {
    val u  = User("u1", "Ada")
    val u2 = u.copy(email = "ada@example.com")    // immutable update
    println(describe(Result.Ok(u2)))
    println(Role.Admin.isPrivileged)
}

List · Map · SetCollections

listOf(1, 2, 3)Read-only List.
mutableListOf<Int>()Mutable variant.
mapOf("a" to 1, "b" to 2)Read-only Map. a to b = Pair(a, b).
setOf(1, 2, 3)Read-only Set.
xs.map { it * 2 }.filter { it > 5 }Eager pipeline. Materializes each stage.
xs.asSequence().map ….toList()Lazy version. Avoids intermediate lists.
xs.groupBy { it.role }Group into Map<K, List<V>>.
xs.associateBy { it.id }Index by key. Returns Map<K, V>.
xs.fold(0) { acc, x -> acc + x }Reduce with explicit initial.
xs.partition { it > 0 }Split into matching / non-matching pair.
xs.windowed(3, 1) / chunked(2)Sliding window / non-overlapping chunks.
xs.zip(ys) / unzip()Pair up / pull apart.

let · run · apply · also · withScope functions

obj.let { it -> … }Transform via lambda. Returns lambda result. Null-guard idiom.
obj.run { this -> … }Block-as-expression. Receiver is this.
obj.apply { this -> … }Preferred Configure-and-return. Returns receiver.
obj.also { it -> … }Side effects. Returns receiver. Good for logging in chains.
with(obj) { … }Group calls on a receiver. Not an extension.
takeIf { … } / takeUnless { … }Returns value when predicate matches / doesn’t.

Worked example

kotlin
// Scope function quick-reference
//   let     -> it,   returns lambda value     (transform, null-guard)
//   run     -> this, returns lambda value     (block-as-expression)
//   apply   -> this, returns receiver         (configure-and-return)
//   also    -> it,   returns receiver         (side-effects, logging)
//   with    -> this, returns lambda value     (group calls on a receiver)

data class Server(var host: String = "", var port: Int = 0, var tls: Boolean = false)

fun main() {
    // apply — configure-and-return
    val s = Server().apply {
        host = "api.devshelf.io"
        port = 443
        tls  = true
    }

    // also — side effects without breaking the chain
    val parsed = "42".toIntOrNull()
        ?.also { println("parsed: $it") }
        ?.let { it * 2 }
        ?: 0

    // run — block-as-expression on a receiver
    val description = s.run { "$host:$port${if (tls) " (tls)" else ""}" }

    // let — null guard + transform
    val email: String? = "ada@example.com"
    email?.let { println("send to: $it") }

    println("parsed=$parsed  desc=$description")
}

Async without callbacksCoroutines

suspend fun fetch(): StringSuspending function. Can only be called from another suspend / coroutine.
coroutineScope { … }Structured concurrency. Waits for children, propagates errors.
runBlocking { … }Bridge to non-suspend code. Use in main + tests.
launch { … }Fire-and-forget. Returns Job.
async { … }.await()Has a result. Use for parallel decomposition.
withContext(Dispatchers.IO) { … }Switch dispatcher (Default / IO / Main / Unconfined).
delay(100.milliseconds)Non-blocking sleep.
withTimeout(2.seconds) { … }Throws on overrun. withTimeoutOrNull returns null instead.
try { … } catch (e: CancellationException) { throw e }Always rethrow cancellation — don’t swallow it.
supervisorScope { … }Failure in one child doesn’t cancel siblings.

Flow

flow { emit(1); emit(2) }Cold, suspending stream.
flowOf(1, 2, 3) / xs.asFlow()Convenience builders.
f.map { … }.filter { … }.collect { … }Pipeline + terminal collector.
f.flowOn(Dispatchers.IO)Run upstream on IO; downstream on caller’s context.
f.catch { e -> … }Handle upstream exceptions.
StateFlow / SharedFlowHot flows: value-current / multi-subscriber.
f.stateIn(scope, …) / shareIn(scope, …)Convert cold flow to hot.
combine(a, b) { x, y -> … }Latest-value zip across two flows.

Variance · reifiedGenerics

class Box<T>(val value: T)Generic class.
fun <T> first(xs: List<T>): T = xs[0]Generic function.
<T : Comparable<T>>Upper bound. Multi-bound with where T : A, T : B.
List<out T>Covariant. Producer of T.
Comparator<in T>Contravariant. Consumer of T.
inline fun <reified T> isA(x: Any) = x is Treified keeps the type at runtime. Inline-only.
List<*>Star-projection. Read-only any-T.

Exceptions · ResultErrors

throw IllegalArgumentException("…")All exceptions are unchecked.
try { … } catch (e: IOException) { … } finally { … }Standard handler.
val v = try { compute() } catch (e: E) { default }try-as-expression. Idiomatic.
require(x > 0) { "x must be positive" }Precondition. Throws IllegalArgumentException.
check(state == Ready)State check. Throws IllegalStateException.
runCatching { … }.onSuccess { … }.onFailure { … }Wraps a block in Result<T>.
.getOrNull() / getOrElse { e -> … }Extract value safely.
@Throws(IOException::class) fun …Tell Java callers what to expect (interop).

Coroutines + Ktor + serialization · ~40 linesEnd-to-end · Concurrent HTTP

Ktor client + kotlinx.serialization. Fan-out three GitHub API calls concurrently with async, parse JSON into a data class, handle per-call failures with runCatching.

kotlin
// build.gradle.kts:
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
// implementation("io.ktor:ktor-client-core:2.3.10")
// implementation("io.ktor:ktor-client-cio:2.3.10")
// implementation("io.ktor:ktor-client-content-negotiation:2.3.10")
// implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.10")

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable

@Serializable
data class Repo(val name: String, val stargazers_count: Int)

suspend fun fetchRepos(client: HttpClient, owner: String): List =
    client.get("https://api.github.com/users/$owner/repos?per_page=5") {
        header("User-Agent", "devshelf")
    }.body()

fun main() = runBlocking {
    val client = HttpClient(CIO) {
        install(ContentNegotiation) { json() }
    }
    val owners = listOf("JetBrains", "Kotlin", "ktorio")

    // Concurrent fan-out via async/await
    val results = owners.map { owner ->
        async { runCatching { fetchRepos(client, owner) } }
    }.awaitAll()

    owners.zip(results).forEach { (owner, res) ->
        res.onSuccess { it.forEach { r -> println("$owner/${r.name} ★${r.stargazers_count}") } }
           .onFailure { System.err.println("$owner: ${it.message}") }
    }
    client.close()
}

Best practiceGood to know

Default to val. Immutability is the language’s preferred mode — the compiler can smart-cast, IDE inspections lighten up, and you avoid a class of bugs where mutated state escapes a coroutine.
Use data class for DTOs, sealed interface for states. Data classes give equals/copy for free; sealed hierarchies turn when into a checked finite-state machine without an else branch.
Coroutines are cheap, but contexts aren’t. launch/async are nearly free. withContext(Dispatchers.IO) for every tiny operation isn’t — batch I/O calls inside one context switch.

Common trapsWatch out for

!! hides bugs. It silences null safety but converts compile-time guarantees into runtime crashes. Almost always there’s a ?.let, Elvis, or requireNotNull that’s clearer about intent.
Catching Throwable swallows cancellation. Coroutines signal cancellation via CancellationException — a broad catch stops it propagating and your scope hangs. Catch specific types, or rethrow it explicitly.
Platform types from Java are nullable in disguise. Calling Java APIs that don’t use @Nullable annotations gives you T! — the compiler trusts you. NPEs leak in this way. Add explicit nullability checks at the boundary.

Go deeperSee also

Kotlin FAQ

What is Kotlin and who makes it?

Kotlin is a statically typed, JVM-compatible language developed by JetBrains and adopted by Google as the preferred language for Android development. It compiles to JVM bytecode, JavaScript, or native binaries (Kotlin Multiplatform). Kotlin is fully interoperable with Java — you can call Java libraries from Kotlin and vice versa.

How does Kotlin handle null safety?

Every type in Kotlin is non-nullable by default. To allow null, declare the type with a ?: String?. Access nullable values with the safe call operator ?. (name?.length returns null instead of throwing) or the Elvis operator ?: (name?.length ?: 0 returns 0 if null). The !! operator forces a non-null assertion and throws NPE if null.

What are Kotlin coroutines?

Coroutines are Kotlin's lightweight concurrency primitive. A coroutine is a suspendable computation — it can pause (suspend) at certain points without blocking the thread, then resume later. Launch starts a fire-and-forget coroutine; async returns a Deferred whose result you await. Coroutines run on a CoroutineScope that controls their lifetime.

What is the difference between val and var in Kotlin?

val declares a read-only (immutable) reference — like final in Java. Once assigned, the reference cannot be reassigned (though the object itself may still be mutable). var declares a mutable reference that can be reassigned freely. Prefer val by default; use var only when reassignment is truly needed.

Is Kotlin fully compatible with Java?

Yes. Kotlin compiles to the same JVM bytecode as Java and can call any Java library directly without adapters. Java code can call Kotlin code too, though some Kotlin features (extension functions, default parameters) have slightly different Java-facing signatures. Kotlin and Java files can coexist in the same project and the same package.

What are Kotlin scope functions?

Scope functions (let, run, with, apply, also) execute a block of code in the context of an object and differ in how they refer to the object (it vs this) and what they return (the object vs the result). apply and also return the receiver — useful for builder-style configuration. let and run return the block result — useful for transformations and null checks.