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
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? = null
Nullable.
s?.length
Safe call. Returns null if s is null.
s?.length ?: 0
Elvis. Default when null.
s!!.length
Force-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 + b
Single-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 syntax
If 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) = user
Destructuring 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(): String
Suspending 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.
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.
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.
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.