You’ve seen Kotlin mentioned in passing.
- “Modern Java,” they said.
- “Null-safe,” they promised.
- “Works on the JVM, Android, browser, fridge, whatever,” they muttered.
So here’s the deal: we’ll learn Kotlin quickly and swiftly (did you see the pun here? hahah so funny). No corporate slides, no Android Studio screenshots, no JetBrains marketing. Just the language, the vibe, and a bit of sarcasm to keep you awake.
The quick pitch
Kotlin is:
- compiled, statically typed, boring in a good way,
- plays nicely with Java (you can literally call
.javaClass
on anything), - and makes
NullPointerException
feel like a fever dream from 2004.
You can use it for Android, backend, CLI tools, or even write multiplatform code that pretends to work on iOS. (pretends.)
The very basics
You’ve seen var
and val
before, but let’s be formal:
val immutable = 42 // can’t reassign
var mutable = 0 // can change
mutable += 1
Type inference? Yes. Explicit types? Also yes. And you’ll end up mixing both like everyone else.
Strings are smart-ish
val who = "world"
println("Hello, $who! 2 + 2 = ${2 + 2}")
See those $
templates? That’s Kotlin being friendly — until you forget the {}
and it just prints $who}
like it’s mocking you.
Null safety, the one real feature
val name: String? = getMaybe()
val len = name?.length ?: 0
That ?.
operator means “don’t explode if it’s null.”
The ?:
means “give me something else instead.”
And if you really want to crash:
val sure = name!!.length // yes, I *swear* it’s not null
Kotlin gives you rope, but it’s padded.
Functions (short and sweet)
fun add(a: Int, b: Int) = a + b
fun greet(name: String = "world") { println("Hi $name") }
You can omit the return type if it’s obvious. Or not. Kotlin won’t judge. It’ll just infer and move on.
if
is an expression (not just a statement)
val mood = if (hour < 12) "☕" else "🔥"
Everything’s an expression here. You’ll start writing when
instead of switch
and feel smug.
val response = when (status) {
200 -> "OK"
in 300..399 -> "Redirect"
else -> "Nope"
}
Yes, in
works on ranges. No, it’s not magic. It’s just operator overloading. We’ll get there.
Loops, briefly
for (i in 0 until 10) println(i)
for (i in 10 downTo 1 step 2) println(i)
And for when you don’t care about indexes:
listOf("a", "b", "c").forEachIndexed { i, v -> println("$i -> $v") }
Collections and friends
val nums = listOf(1, 2, 3, 4)
val evens = nums.filter { it % 2 == 0 }
val squares = nums.map { it * it }
val sum = nums.reduce { acc, n -> acc + n }
Everything is immutable
by default. Want chaos?
Use mutableListOf()
. Then question your decisions.
Data classes: records that actually work
data class User(val id: Int, val name: String)
val u1 = User(1, "Sam")
val u2 = u1.copy(name = "Samuel")
They give you equals
, hashCode
, toString
, copy
, and destructuring.
All without writing 70 lines of boilerplate.
(Java intensifies in the distance.)
Objects, companions, singletons — oh my
object Log { fun d(msg: String) = println(msg) }
class C { companion object { fun make() = C() } }
Kotlin’s object
is a singleton.
companion object
is a singleton inside your class, because why not.
Extensions (monkey patching, but polite)
fun String.title(): String =
split(" ").joinToString(" ") { it.replaceFirstChar(Char::titlecase) }
"hello kotlin".title() // "Hello Kotlin"
You didn’t subclass String
. You just extended it.
Congratulations, you’ve made a DSL.
Operator overloading (the tasteful kind)
data class Vec(val x: Int, val y: Int)
operator fun Vec.plus(o: Vec) = Vec(x + o.x, y + o.y)
val v = Vec(1,2) + Vec(3,4)
Readable math. Dangerous power. Use responsibly.
Sealed hierarchies (for people who liked Rust enums)
sealed interface Result<out T>
data class Ok<T>(val value: T): Result<T>
data class Err(val error: Throwable): Result<Nothing>
When you when
, the compiler forces you to handle all cases.
It’s Kotlin’s way of saying “don’t forget the sad path.”
Coroutines (aka async for grownups)
They look like magic. They’re just suspend
functions and structured concurrency done right.
import kotlinx.coroutines.*
suspend fun fetch(id: Int): String = withContext(Dispatchers.IO) {
delay(100)
"item-$id"
}
suspend fun parallel(): List<String> = coroutineScope {
(1..3).map { async { fetch(it) } }.awaitAll()
}
fun main() = runBlocking {
println(parallel())
}
It’s concurrency you can actually read.
(Unlike Java’s Future
API, which was written by demons.)
“Try or die” is dead. Meet runCatching
.
val res = runCatching { risky() }
.getOrElse { println("nope"); -1 }
Exceptions become values. You stop writing try/catch/finally
pyramids.
Everyone wins, except StackOverflow copy/pasters.
Builders, DSLs, and why Kotlin secretly loves HTML
fun html(init: Html.() -> Unit) = Html().apply(init)
class Html {
private val out = StringBuilder()
fun div(init: Html.() -> Unit) { out.append("<div>"); init(); out.append("</div>") }
fun text(s: String) { out.append(s) }
override fun toString() = out.toString()
}
val h = html { div { text("Hello DSL") } }
See? You just built a templating language. Accidentally.
Things Kotlin does better than Java
- Null safety that actually works.
- Default arguments.
- Smart casts (
if (x is String)
→x.length
just works). - Lambdas that don’t make you cry.
- Coroutines that don’t spawn 18 threads per task.
- 80% less
public static final void
.
Things Kotlin does worse than Rust
- Memory safety (it has a GC, deal with it).
- Performance (JVM is fast, but not that fast).
- Compile times. Don’t ask.
- Tooling that doesn’t occasionally scream about Gradle caches.
The full-circle example
Let’s end with something “realish”:
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
sealed interface Fetch<out T> {
data class Ok<T>(val v: T): Fetch<T>
data class Err(val msg: String): Fetch<Nothing>
}
data class Post(val id: Int, val title: String)
suspend fun fetchPost(id: Int): Fetch<Post> = withContext(IO) {
runCatching {
delay(50)
Post(id, "Hello Kotlin")
}.fold(
onSuccess = { Fetch.Ok(it) },
onFailure = { Fetch.Err(it.message ?: "unknown") }
)
}
suspend fun fetchAll(ids: List<Int>): List<Post> = coroutineScope {
ids.map { async { fetchPost(it) } }.awaitAll().mapNotNull {
when (it) {
is Fetch.Ok -> it.v
is Fetch.Err -> null
}
}
}
fun main() = runBlocking {
println(fetchAll((1..3).toList()))
}
It fetches things, handles errors, runs concurrently, and still fits in a tweet-thread screenshot. That’s Kotlin.
Where to go next
Search for:
- “Kotlin coroutines structured concurrency”
- “Ktor client/server”
- “Kotlin sealed classes vs enums”
- “Arrow-kt” if you want the FP rabbit hole
Or just start writing. Kotlin rewards doing, not reading.