Control flow

when is Kotlin’s powerful replacement for switch, and expressions are first-class.

Kotlin
fun describe(obj: Any): String = when (obj) {
    1          -> "One"
    "Hello"    -> "Greeting"
    is Long    -> "Long number"
    !is String -> "Not a string"
    else       -> "Unknown"
}

val max = if (a > b) a else b   // if is an expression

Collections & functional style

Kotlin
val numbers = listOf(1, 2, 3, 4, 5, 6)

val evens = numbers.filter { it % 2 == 0 }
val squared = numbers.map { it * it }
val sum = numbers.reduce { acc, n -> acc + n }

val grouped = numbers.groupBy { it % 2 == 0 }
// {false=[1, 3, 5], true=[2, 4, 6]}

numbers
    .asSequence()
    .filter { it > 2 }
    .map { it * 10 }
    .toList()

Extension functions

Add new functions to existing types without inheritance.

Kotlin
fun String.isPalindrome(): Boolean {
    val cleaned = this.filter { it.isLetterOrDigit() }.lowercase()
    return cleaned == cleaned.reversed()
}

println("A man a plan a canal Panama".isPalindrome())  // true

Data classes & destructuring

Kotlin
data class User(val id: Int, val name: String, val email: String)

val user = User(1, "Ada", "ada@example.com")
val (id, name, email) = user

println(user.copy(name = "Augusta"))
// User(id=1, name=Augusta, email=ada@example.com)

Sealed classes & exhaustive when

Kotlin
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun handle(result: Result<String>) = when (result) {
    is Result.Success -> println(result.data)
    is Result.Error   -> println("Error: ${result.message}")
    Result.Loading     -> println("Loading…")
}  // no else needed — exhaustive

Coroutines (async made simple)

Lightweight threads with structured concurrency. Add the dependency first:

build.gradle.kts
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
Kotlin
import kotlinx.coroutines.*

suspend fun fetchUser(id: Int): String {
    delay(1000)  // non-blocking
    return "User-$id"
}

fun main() = runBlocking {
    val deferred = async { fetchUser(42) }
    println("Doing other work…")
    println(deferred.await())
}

Scope functions

let, run, with, apply, and also keep code fluent and null-safe.

Kotlin
val result = name?.let {
    println("Name length: ${it.length}")
    it.uppercase()
}

val person = Person().apply {
    this.name = "Grace"
    this.age = 45
}.also {
    println("Created: $it")
}

Interfaces & default methods

Kotlin
interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}

class Button : Clickable {
    override fun click() = println("Button clicked")
}

Practical mini-project

A tiny CLI todo list using a mutable list and simple commands:

Todo.kt
fun main() {
    val todos = mutableListOf<String>()
    println("Commands: add <text> | list | done <n> | quit")

    while (true) {
        print("> ")
        val input = readlnOrNull() ?: break
        val parts = input.trim().split(" ", limit = 2)
        when (parts[0].lowercase()) {
            "add"  -> parts.getOrNull(1)?.let { todos += it; println("Added.") }
            "list" -> todos.forEachIndexed { i, t -> println("${i + 1}. $t") }
            "done" -> parts.getOrNull(1)?.toIntOrNull()?.let {
                if (it in 1..todos.size) {
                    println("Done: ${todos.removeAt(it - 1)}")
                }
            }
            "quit" -> break
            else   -> println("Unknown command")
        }
    }
}

Where to go deeper