Kotlin is a modern, concise programming language that runs on the JVM, compiles to JavaScript, and targets native platforms. It's Google's preferred language for Android development and increasingly popular for server-side development with frameworks like Spring Boot and Ktor. This tutorial takes you from zero to writing real Kotlin programs.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Install Kotlin and write your first program |
| Syntax & variables | Understand Kotlin's concise syntax vs Java |
| Null safety | Eliminate NullPointerExceptions at compile time |
| OOP | Write classes, data classes, sealed classes |
| Functional programming | Use lambdas, higher-order functions, collections |
| Coroutines | Write async code that reads like sync code |
| Real projects | Build CLI apps and REST APIs |
Kotlin vs Java vs Python
| Feature | Kotlin | Java | Python |
|---|---|---|---|
| Verbosity | Concise | Verbose | Concise |
| Null safety | Built-in | Optional (annotations) | Runtime only |
| Type system | Static, inferred | Static, explicit | Dynamic |
| Interoperability | 100% Java compat | — | Limited JVM |
| Android | Preferred | Legacy | Limited |
| Learning curve | Moderate | Moderate | Easy |
| Performance | JVM speed | JVM speed | Slower |
| Coroutines | First-class | Project Loom | asyncio |
Setup
Option 1: IntelliJ IDEA (recommended)
- Download IntelliJ IDEA Community (free)
- Create a new Kotlin project (File → New → Project → Kotlin)
- IntelliJ bundles the Kotlin compiler — no separate install needed
Option 2: Command line
macOS (Homebrew):
brew install kotlin
Linux (SDKMAN):
curl -s "https://get.sdkman.io" | bash
sdk install kotlin
Windows: Download from kotlinlang.org/docs/command-line.html
Verify:
kotlinc -version
# kotlin-compiler 2.0.x
Option 3: Online playground
Try Kotlin instantly at play.kotlinlang.org — no install needed.
Your first Kotlin program
fun main() {
println("Hello, Kotlin!")
}
Compile and run:
kotlinc hello.kt -include-runtime -d hello.jar
java -jar hello.jar
# Hello, Kotlin!
Notice: no semicolons, no public static void main, no System.out.println — Kotlin is much more concise than Java.
Variables and Types
Kotlin has two keywords for variables:
val name = "Alice" // val = immutable (like final in Java)
var age = 30 // var = mutable
age = 31 // OK
// name = "Bob" // Compile error — val cannot be reassigned
Explicit types:
val message: String = "Hello"
var count: Int = 0
val pi: Double = 3.14159
val isActive: Boolean = true
val letter: Char = 'K'
Type inference — Kotlin infers the type from the value:
val city = "Podgorica" // String inferred
val temperature = 36.5 // Double inferred
val population = 1_000_000 // Int inferred (underscores OK in numbers)
All basic types
| Type | Size | Range / Notes |
|---|---|---|
Byte |
8-bit | -128 to 127 |
Short |
16-bit | -32,768 to 32,767 |
Int |
32-bit | -2^31 to 2^31-1 |
Long |
64-bit | -2^63 to 2^63-1 |
Float |
32-bit | 6-7 decimal digits |
Double |
64-bit | 15-16 decimal digits |
Char |
16-bit | Single Unicode character |
Boolean |
— | true or false |
String |
— | Immutable text |
Number literals:
val million = 1_000_000 // readable with underscores
val hexColor = 0xFF_EC_D2 // hex
val binary = 0b0001_0101 // binary
val bigLong = 1_000_000_000L // Long suffix
val pi = 3.14f // Float suffix
Strings
val first = "Alice"
val last = "Smith"
// String templates — use $ and ${}
val greeting = "Hello, $first $last!"
val upper = "Name in uppercase: ${first.uppercase()}"
// Multi-line strings
val poem = """
Roses are red,
Kotlin is great,
No NPEs today.
""".trimIndent()
println(greeting) // Hello, Alice Smith!
println(upper) // Name in uppercase: ALICE
Common string operations:
val s = " Hello, World! "
println(s.trim()) // "Hello, World!"
println(s.lowercase()) // " hello, world! "
println(s.uppercase()) // " HELLO, WORLD! "
println(s.replace("World", "Kotlin")) // " Hello, Kotlin! "
println(s.contains("World")) // true
println(s.startsWith(" H")) // true
println(s.split(", ")) // [ Hello, World! ]
println(s.length) // 17
println(s.trim().substring(0, 5)) // "Hello"
Null Safety
Null safety is Kotlin's killer feature — it eliminates NullPointerException at compile time.
// Non-nullable: cannot hold null
var name: String = "Alice"
// name = null // Compile error!
// Nullable: add ? to allow null
var nickname: String? = null
nickname = "Ali"
nickname = null // OK
Safe call operator ?.
val name: String? = null
// Safe call — returns null if name is null
println(name?.length) // null (no crash)
println(name?.uppercase()?.trimEnd()) // can chain
Elvis operator ?:
val name: String? = null
val displayName = name ?: "Anonymous" // use "Anonymous" if null
println(displayName) // Anonymous
val length = name?.length ?: 0 // 0 if name is null
Non-null assertion !!
val name: String? = "Alice"
val length = name!!.length // throws NPE if name is null — use sparingly!
Safe cast as?
val obj: Any = "Hello"
val str: String? = obj as? String // null if cast fails, no exception
val num: Int? = obj as? Int // null — obj is not Int
Let scope function
val email: String? = getUserEmail()
email?.let { address ->
sendEmail(address) // only runs if email is not null
println("Sent to $address")
}
Null safety summary
| Operator | Meaning |
|---|---|
T? |
Nullable type |
?. |
Safe call — returns null if receiver is null |
?: |
Elvis — provide default if null |
!! |
Non-null assertion — throws if null |
as? |
Safe cast — returns null if cast fails |
Control Flow
if / else (also an expression)
val temperature = 25
// Statement form
if (temperature > 30) {
println("Hot!")
} else if (temperature > 20) {
println("Warm")
} else {
println("Cool")
}
// Expression form — if returns a value
val description = if (temperature > 30) "Hot" else "Not hot"
println(description) // Not hot
when (Kotlin's switch, but much more powerful)
val day = 3
val dayName = when (day) {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4, 5 -> "Thursday or Friday"
in 6..7 -> "Weekend"
else -> "Invalid"
}
println(dayName) // Wednesday
// when with no argument (like if-else chain)
val score = 75
val grade = when {
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
score >= 60 -> "D"
else -> "F"
}
// when with type checking
fun describe(obj: Any): String = when (obj) {
is Int -> "Integer: $obj"
is String -> "String of length ${obj.length}"
is List<*> -> "List with ${obj.size} elements"
else -> "Unknown"
}
println(describe(42)) // Integer: 42
println(describe("Hello")) // String of length 5
Loops
// for loop with range
for (i in 1..5) {
print("$i ") // 1 2 3 4 5
}
// Exclusive range
for (i in 1 until 5) {
print("$i ") // 1 2 3 4
}
// Step
for (i in 0..10 step 2) {
print("$i ") // 0 2 4 6 8 10
}
// Countdown
for (i in 5 downTo 1) {
print("$i ") // 5 4 3 2 1
}
// Iterate over collection
val fruits = listOf("apple", "banana", "cherry")
for (fruit in fruits) {
println(fruit)
}
// With index
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit")
}
// while
var count = 0
while (count < 5) {
println(count)
count++
}
// do-while
do {
println("Runs at least once")
count--
} while (count > 0)
// break and continue
for (i in 1..10) {
if (i == 3) continue // skip 3
if (i == 7) break // stop at 7
print("$i ") // 1 2 4 5 6
}
Functions
// Basic function
fun greet(name: String): String {
return "Hello, $name!"
}
// Single-expression function
fun greet(name: String) = "Hello, $name!"
// Default parameters
fun greet(name: String, greeting: String = "Hello") = "$greeting, $name!"
// Named arguments
println(greet(name = "Alice", greeting = "Hi"))
println(greet(greeting = "Bonjour", name = "Pierre"))
// Vararg
fun sum(vararg numbers: Int): Int = numbers.sum()
println(sum(1, 2, 3, 4, 5)) // 15
// Spread operator for arrays
val nums = intArrayOf(1, 2, 3)
println(sum(*nums)) // 6
Extension functions
Extend existing classes without inheriting from them:
// Add a function to String
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}
println("racecar".isPalindrome()) // true
println("hello".isPalindrome()) // false
// Add a function to Int
fun Int.isEven() = this % 2 == 0
println(4.isEven()) // true
// Extension on nullable type
fun String?.orEmpty() = this ?: ""
val name: String? = null
println(name.orEmpty()) // "" (standard library already has this)
Infix functions
infix fun Int.plus(other: Int) = this + other
val result = 3 plus 4 // reads like natural language
println(result) // 7
// Standard library infix: to (for pairs)
val pair = "key" to "value" // Pair<String, String>
val map = mapOf("a" to 1, "b" to 2)
Higher-order functions
Functions that take functions as parameters or return functions:
fun applyTwice(x: Int, operation: (Int) -> Int): Int {
return operation(operation(x))
}
val double = { n: Int -> n * 2 }
println(applyTwice(3, double)) // 12
// Lambda shorthand — last parameter can be outside parentheses
println(applyTwice(3) { it * 2 }) // 12
Classes and OOP
class Person(val name: String, var age: Int) {
// Secondary constructor
constructor(name: String) : this(name, 0)
// Method
fun greet() = "Hi, I'm $name, $age years old."
// Property with custom getter
val isAdult: Boolean
get() = age >= 18
// Property with getter and setter
var email: String = ""
set(value) {
field = value.trim().lowercase()
}
override fun toString() = "Person(name=$name, age=$age)"
}
val alice = Person("Alice", 30)
println(alice.greet()) // Hi, I'm Alice, 30 years old.
println(alice.isAdult) // true
alice.email = " Alice@Example.com "
println(alice.email) // alice@example.com
Data classes
Auto-generate equals(), hashCode(), toString(), copy(), and destructuring:
data class Point(val x: Int, val y: Int)
val p1 = Point(1, 2)
val p2 = Point(1, 2)
val p3 = p1.copy(y = 10)
println(p1) // Point(x=1, y=2)
println(p1 == p2) // true (structural equality)
println(p3) // Point(x=1, y=10)
// Destructuring
val (x, y) = p1
println("x=$x, y=$y") // x=1, y=2
Inheritance
open class Animal(val name: String) {
open fun sound(): String = "..."
fun describe() = "$name says ${sound()}"
}
class Dog(name: String) : Animal(name) {
override fun sound() = "Woof"
}
class Cat(name: String) : Animal(name) {
override fun sound() = "Meow"
}
val dog = Dog("Rex")
val cat = Cat("Whiskers")
println(dog.describe()) // Rex says Woof
println(cat.describe()) // Whiskers says Meow
Abstract classes and interfaces
abstract class Shape {
abstract fun area(): Double
abstract fun perimeter(): Double
fun describe() = "Area: ${area()}, Perimeter: ${perimeter()}"
}
class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
override fun perimeter() = 2 * Math.PI * radius
}
class Rectangle(val width: Double, val height: Double) : Shape() {
override fun area() = width * height
override fun perimeter() = 2 * (width + height)
}
// Interface — can have default implementations
interface Drawable {
fun draw()
fun resize(factor: Double) {
println("Resizing by $factor") // default implementation
}
}
interface Colorable {
var color: String
}
class Square(val side: Double) : Shape(), Drawable, Colorable {
override var color = "blue"
override fun area() = side * side
override fun perimeter() = 4 * side
override fun draw() = println("Drawing a $color square")
}
Object declarations and companion objects
// Singleton
object DatabaseConfig {
val host = "localhost"
val port = 5432
fun connectionString() = "jdbc:postgresql://$host:$port/mydb"
}
println(DatabaseConfig.connectionString())
// Companion object (like static in Java)
class MathUtils {
companion object {
const val PI = 3.14159
fun circleArea(radius: Double) = PI * radius * radius
// Factory method
fun create() = MathUtils()
}
}
println(MathUtils.PI)
println(MathUtils.circleArea(5.0))
val instance = MathUtils.create()
Sealed classes
Restrict class hierarchies — all subclasses must be in the same file:
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 handleResult(result: Result<String>) = when (result) {
is Result.Success -> println("Got: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
// No else needed — compiler knows all cases
}
handleResult(Result.Success("Hello")) // Got: Hello
handleResult(Result.Error("Network error")) // Error: Network error
handleResult(Result.Loading) // Loading...
Enum classes
enum class Direction {
NORTH, SOUTH, EAST, WEST;
fun opposite(): Direction = when (this) {
NORTH -> SOUTH
SOUTH -> NORTH
EAST -> WEST
WEST -> EAST
}
}
val dir = Direction.NORTH
println(dir.opposite()) // SOUTH
println(Direction.values().toList()) // [NORTH, SOUTH, EAST, WEST]
// Enum with properties
enum class Planet(val mass: Double, val radius: Double) {
MERCURY(3.303e+23, 2.4397e6),
VENUS(4.869e+24, 6.0518e6),
EARTH(5.976e+24, 6.37814e6);
val gravity = 6.67300e-11 * mass / (radius * radius)
}
println(Planet.EARTH.gravity) // ~9.8
Collections
Kotlin distinguishes between read-only and mutable collections:
// Read-only
val names = listOf("Alice", "Bob", "Charlie")
val scores = mapOf("Alice" to 95, "Bob" to 87)
val tags = setOf("kotlin", "jvm", "android")
// Mutable
val mutableNames = mutableListOf("Alice", "Bob")
mutableNames.add("Charlie")
mutableNames.remove("Bob")
val mutableMap = mutableMapOf("a" to 1)
mutableMap["b"] = 2
mutableMap.remove("a")
val mutableSet = mutableSetOf(1, 2, 3)
mutableSet.add(4)
List operations
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// filter — keep elements that match predicate
val evens = numbers.filter { it % 2 == 0 }
println(evens) // [2, 4, 6, 8, 10]
// map — transform each element
val doubled = numbers.map { it * 2 }
println(doubled) // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// reduce / fold
val sum = numbers.reduce { acc, n -> acc + n }
println(sum) // 55
val sumWithFold = numbers.fold(0) { acc, n -> acc + n }
// any / all / none
println(numbers.any { it > 9 }) // true
println(numbers.all { it > 0 }) // true
println(numbers.none { it < 0 }) // true
// find / firstOrNull
val firstEven = numbers.firstOrNull { it % 2 == 0 }
println(firstEven) // 2
// groupBy
val grouped = numbers.groupBy { if (it % 2 == 0) "even" else "odd" }
println(grouped) // {odd=[1, 3, 5, 7, 9], even=[2, 4, 6, 8, 10]}
// sortedBy / sortedByDescending
data class Person(val name: String, val age: Int)
val people = listOf(Person("Bob", 30), Person("Alice", 25), Person("Charlie", 35))
val sorted = people.sortedBy { it.name }
val byAgeDesc = people.sortedByDescending { it.age }
// flatMap — map then flatten
val words = listOf("Hello World", "Kotlin Is Great")
val letters = words.flatMap { it.split(" ") }
println(letters) // [Hello, World, Kotlin, Is, Great]
// take / drop
println(numbers.take(3)) // [1, 2, 3]
println(numbers.drop(7)) // [8, 9, 10]
// zip
val keys = listOf("a", "b", "c")
val values = listOf(1, 2, 3)
val zipped = keys.zip(values)
println(zipped) // [(a, 1), (b, 2), (c, 3)]
Map operations
val inventory = mapOf("apple" to 50, "banana" to 30, "cherry" to 20)
// Access
println(inventory["apple"]) // 50
println(inventory.getOrDefault("mango", 0)) // 0
println(inventory.getOrElse("mango") { 0 }) // 0
// Iteration
for ((item, count) in inventory) {
println("$item: $count")
}
// Filter and transform
val abundant = inventory.filter { (_, count) -> count > 25 }
println(abundant) // {apple=50, banana=30}
val doubled = inventory.mapValues { (_, v) -> v * 2 }
println(doubled) // {apple=100, banana=60, cherry=40}
// Convert to list of pairs
val pairs = inventory.toList()
val sorted = inventory.toSortedMap()
Lambdas and Functional Programming
// Lambda syntax
val square: (Int) -> Int = { x -> x * x }
val add: (Int, Int) -> Int = { a, b -> a + b }
// Single parameter: use 'it'
val isEven: (Int) -> Boolean = { it % 2 == 0 }
println(square(5)) // 25
println(add(3, 4)) // 7
println(isEven(6)) // true
// Function references
fun triple(x: Int) = x * 3
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.map(::triple)) // [3, 6, 9, 12, 15]
println(numbers.map { it * 3 }) // same result
// Returning functions
fun multiplier(factor: Int): (Int) -> Int = { it * factor }
val double = multiplier(2)
val triple2 = multiplier(3)
println(double(5)) // 10
println(triple2(5)) // 15
Scope functions
Kotlin's scope functions run a block on an object:
| Function | Context | Returns | Use when |
|---|---|---|---|
let |
it |
Lambda result | Transform nullable, chain calls |
run |
this |
Lambda result | Object config + result |
with |
this |
Lambda result | Multiple operations on object |
apply |
this |
Object itself | Object initialization |
also |
it |
Object itself | Side effects (logging) |
data class Person(var name: String = "", var age: Int = 0, var email: String = "")
// apply — configure an object
val alice = Person().apply {
name = "Alice"
age = 30
email = "alice@example.com"
}
println(alice)
// let — transform or run on non-null
val result = "hello world"
.let { it.split(" ") }
.let { it.map { word -> word.capitalize() } }
.let { it.joinToString(" ") }
println(result) // Hello World
// also — side effects, returns original
val numbers = mutableListOf(1, 2, 3)
.also { println("Before: $it") } // Before: [1, 2, 3]
.apply { add(4) }
.also { println("After: $it") } // After: [1, 2, 3, 4]
// with — multiple operations on an object
val html = with(StringBuilder()) {
append("<html>")
append("<body>Hello</body>")
append("</html>")
toString()
}
println(html)
// run — config + return result
val port = System.getenv("PORT")?.run { toInt() } ?: 8080
Generics
// Generic function
fun <T> swap(a: T, b: T): Pair<T, T> = Pair(b, a)
val swapped = swap(1, 2)
println(swapped) // (2, 1)
// Generic class
class Box<T>(var value: T) {
fun get(): T = value
fun set(newValue: T) { value = newValue }
override fun toString() = "Box($value)"
}
val intBox = Box(42)
val strBox = Box("Hello")
println(intBox) // Box(42)
// Type constraints
fun <T : Comparable<T>> max(a: T, b: T): T = if (a > b) a else b
println(max(3, 7)) // 7
println(max("abc", "xyz")) // xyz
// In / Out variance
interface Producer<out T> {
fun produce(): T
}
interface Consumer<in T> {
fun consume(item: T)
}
Coroutines
Kotlin coroutines make asynchronous programming simple — write async code that looks synchronous.
Setup
Add to build.gradle.kts:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
Basic coroutines
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Start")
// launch — fire and forget
val job = launch {
delay(1000L) // non-blocking delay
println("Coroutine done!")
}
println("Waiting...")
job.join() // wait for job to complete
println("End")
}
// Output:
// Start
// Waiting...
// (1 second pause)
// Coroutine done!
// End
async / await
import kotlinx.coroutines.*
suspend fun fetchUser(): String {
delay(1000L) // simulate network call
return "Alice"
}
suspend fun fetchScore(): Int {
delay(500L)
return 95
}
fun main() = runBlocking {
// Sequential — takes 1500ms
val user = fetchUser()
val score = fetchScore()
// Parallel with async — takes ~1000ms (longest)
val userDeferred = async { fetchUser() }
val scoreDeferred = async { fetchScore() }
val user2 = userDeferred.await()
val score2 = scoreDeferred.await()
println("$user2: $score2")
}
Suspend functions
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// suspend — can be paused without blocking a thread
suspend fun downloadFile(url: String): ByteArray {
delay(2000L) // simulate download
return ByteArray(1024)
}
// Flow — cold stream of values
fun countFrom(start: Int): Flow<Int> = flow {
for (i in start..start + 4) {
delay(100L)
emit(i)
}
}
fun main() = runBlocking {
// Collect flow values
countFrom(1).collect { value ->
println("Got: $value")
}
// Got: 1, Got: 2, Got: 3, Got: 4, Got: 5
}
Coroutine context and dispatchers
import kotlinx.coroutines.*
fun main() = runBlocking {
// Dispatchers.Default — CPU-intensive work
launch(Dispatchers.Default) {
val result = (1..1_000_000).sum()
println("Sum: $result")
}
// Dispatchers.IO — network/disk operations
launch(Dispatchers.IO) {
// database query, HTTP request, file I/O
println("IO work on: ${Thread.currentThread().name}")
}
// Dispatchers.Main — UI thread (Android)
// launch(Dispatchers.Main) { updateUI() }
}
Error Handling
// try-catch-finally
fun divide(a: Int, b: Int): Int {
return try {
a / b
} catch (e: ArithmeticException) {
println("Error: ${e.message}")
-1
} finally {
println("Always runs")
}
}
println(divide(10, 2)) // 5
println(divide(10, 0)) // Error: / by zero, -1
// Custom exception
class ValidationException(message: String, val field: String) : Exception(message)
fun validateAge(age: Int) {
if (age < 0 || age > 150) {
throw ValidationException("Invalid age: $age", "age")
}
}
try {
validateAge(-5)
} catch (e: ValidationException) {
println("Validation failed on ${e.field}: ${e.message}")
}
Result type pattern
fun parseIntSafely(s: String): Result<Int> {
return try {
Result.success(s.toInt())
} catch (e: NumberFormatException) {
Result.failure(e)
}
}
val result = parseIntSafely("42")
result.fold(
onSuccess = { println("Parsed: $it") },
onFailure = { println("Failed: ${it.message}") }
)
// getOrDefault, getOrElse, getOrThrow
val num = parseIntSafely("abc").getOrDefault(0)
println(num) // 0
File I/O
import java.io.File
// Write a file
File("output.txt").writeText("Hello, Kotlin!")
// Append to a file
File("output.txt").appendText("\nSecond line")
// Read entire file
val content = File("output.txt").readText()
println(content)
// Read lines
val lines = File("output.txt").readLines()
lines.forEachIndexed { i, line -> println("$i: $line") }
// Read line by line (memory-efficient for large files)
File("large.txt").forEachLine { line ->
println(line)
}
// Check existence, create directories
val dir = File("data/reports")
dir.mkdirs()
println(dir.exists()) // true
// List files
File(".").listFiles()
?.filter { it.extension == "txt" }
?.forEach { println(it.name) }
Three Real Projects
Project 1: CLI Todo Manager
import java.io.File
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
@Serializable
data class Todo(
val id: Int,
val text: String,
var done: Boolean = false
)
object TodoManager {
private val file = File("todos.json")
private val json = Json { prettyPrint = true }
private fun load(): MutableList<Todo> {
if (!file.exists()) return mutableListOf()
return json.decodeFromString(file.readText())
}
private fun save(todos: List<Todo>) {
file.writeText(json.encodeToString(todos))
}
fun add(text: String) {
val todos = load()
val id = (todos.maxOfOrNull { it.id } ?: 0) + 1
todos.add(Todo(id, text))
save(todos)
println("Added: [$id] $text")
}
fun list() {
val todos = load()
if (todos.isEmpty()) {
println("No todos yet. Add one with: add <text>")
return
}
todos.forEach { todo ->
val status = if (todo.done) "✓" else "○"
println("[$status] ${todo.id}. ${todo.text}")
}
}
fun complete(id: Int) {
val todos = load()
val todo = todos.firstOrNull { it.id == id }
if (todo == null) {
println("Todo $id not found")
return
}
todo.done = true
save(todos)
println("Completed: ${todo.text}")
}
fun delete(id: Int) {
val todos = load()
val removed = todos.removeIf { it.id == id }
if (removed) {
save(todos)
println("Deleted todo $id")
} else {
println("Todo $id not found")
}
}
}
fun main(args: Array<String>) {
when {
args.isEmpty() -> TodoManager.list()
args[0] == "add" && args.size > 1 -> TodoManager.add(args.drop(1).joinToString(" "))
args[0] == "done" && args.size > 1 -> TodoManager.complete(args[1].toInt())
args[0] == "delete" && args.size > 1 -> TodoManager.delete(args[1].toInt())
args[0] == "list" -> TodoManager.list()
else -> println("Usage: todo [list|add <text>|done <id>|delete <id>]")
}
}
# Usage:
kotlin TodoCLI.kt add "Buy groceries"
kotlin TodoCLI.kt add "Write Kotlin tutorial"
kotlin TodoCLI.kt list
# [○] 1. Buy groceries
# [○] 2. Write Kotlin tutorial
kotlin TodoCLI.kt done 1
kotlin TodoCLI.kt list
# [✓] 1. Buy groceries
# [○] 2. Write Kotlin tutorial
Project 2: REST API with Ktor
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-server-netty:2.3.11")
implementation("io.ktor:ktor-server-content-negotiation:2.3.11")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11")
implementation("ch.qos.logback:logback-classic:1.4.14")
}
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
@Serializable
data class Note(val id: Int, val title: String, val content: String)
@Serializable
data class CreateNoteRequest(val title: String, val content: String)
object NoteRepository {
private val notes = ConcurrentHashMap<Int, Note>()
private val counter = AtomicInteger(0)
fun getAll() = notes.values.sortedBy { it.id }
fun get(id: Int) = notes[id]
fun create(req: CreateNoteRequest): Note {
val note = Note(counter.incrementAndGet(), req.title, req.content)
notes[note.id] = note
return note
}
fun delete(id: Int) = notes.remove(id) != null
}
fun main() {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) { json() }
routing {
get("/notes") {
call.respond(NoteRepository.getAll())
}
get("/notes/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid ID")
val note = NoteRepository.get(id)
?: return@get call.respond(HttpStatusCode.NotFound, "Note not found")
call.respond(note)
}
post("/notes") {
val req = call.receive<CreateNoteRequest>()
val note = NoteRepository.create(req)
call.respond(HttpStatusCode.Created, note)
}
delete("/notes/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
?: return@delete call.respond(HttpStatusCode.BadRequest)
if (NoteRepository.delete(id)) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}
}.start(wait = true)
}
# Test the API:
curl http://localhost:8080/notes
# []
curl -X POST http://localhost:8080/notes \
-H "Content-Type: application/json" \
-d '{"title":"First note","content":"Hello Kotlin!"}'
# {"id":1,"title":"First note","content":"Hello Kotlin!"}
curl http://localhost:8080/notes/1
# {"id":1,"title":"First note","content":"Hello Kotlin!"}
curl -X DELETE http://localhost:8080/notes/1
# (204 No Content)
Project 3: Coroutines — Parallel Data Fetcher
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.net.URL
import kotlin.time.measureTime
data class StockPrice(val symbol: String, val price: Double)
// Simulate fetching stock price from an API
suspend fun fetchStockPrice(symbol: String): StockPrice {
delay((200..800).random().toLong()) // simulate network latency
val fakePrice = mapOf(
"AAPL" to 182.50, "GOOGL" to 175.30, "MSFT" to 420.10,
"AMZN" to 195.80, "TSLA" to 248.60
)
return StockPrice(symbol, fakePrice[symbol] ?: 0.0)
}
suspend fun fetchAllPrices(symbols: List<String>): List<StockPrice> = coroutineScope {
symbols.map { symbol ->
async { fetchStockPrice(symbol) } // launch all concurrently
}.awaitAll() // wait for all to complete
}
fun portfolioValue(prices: List<StockPrice>, holdings: Map<String, Int>): Double {
return prices.sumOf { stock ->
stock.price * (holdings[stock.symbol] ?: 0)
}
}
fun main() = runBlocking {
val symbols = listOf("AAPL", "GOOGL", "MSFT", "AMZN", "TSLA")
val holdings = mapOf("AAPL" to 10, "GOOGL" to 5, "MSFT" to 8, "AMZN" to 3, "TSLA" to 15)
println("Fetching prices for ${symbols.size} stocks...")
val elapsed = measureTime {
val prices = fetchAllPrices(symbols)
prices.forEach { println("${it.symbol}: \$${it.price}") }
val total = portfolioValue(prices, holdings)
println("\nPortfolio value: \$${String.format("%.2f", total)}")
}
println("Fetched in $elapsed (parallel)")
}
Kotlin vs Related Terms
| Term | Relationship |
|---|---|
| Kotlin | Programming language targeting JVM, JS, Native |
| JVM | Java Virtual Machine — Kotlin compiles to JVM bytecode |
| Java | Kotlin is 100% interoperable with Java code |
| Android | Google's preferred language for Android since 2017 |
| Kotlin Multiplatform (KMP) | Share business logic across Android, iOS, web |
| Coroutines | Kotlin's structured concurrency library |
| Ktor | Kotlin-native web framework by JetBrains |
| Exposed | Kotlin SQL framework by JetBrains |
| Arrow | Functional programming library for Kotlin |
| Gradle (Kotlin DSL) | Build tool using Kotlin syntax (.kts files) |
Learning Path
| Stage | Topics | Timeframe |
|---|---|---|
| 1. Foundations | Variables, types, null safety, control flow | 1-2 weeks |
| 2. Functions | Lambdas, extension functions, higher-order functions | 1-2 weeks |
| 3. OOP | Classes, data classes, sealed classes, inheritance | 2 weeks |
| 4. Collections | List/Map/Set operations, functional transformations | 1 week |
| 5. Coroutines | suspend, launch, async, Flow | 2-3 weeks |
| 6. Choose path | Android (Jetpack Compose) or Backend (Ktor/Spring Boot) | Ongoing |
| 7. KMP | Share code across platforms | Month 4+ |
Free resources:
- kotlinlang.org/docs — official docs
- play.kotlinlang.org — online playground
- Kotlin Koans — interactive exercises at kotlinlang.org/docs/koans.html
- JetBrains Academy — structured Kotlin courses
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using !! everywhere |
Runtime NPE — defeats null safety | Use ?., ?:, or let |
var instead of val |
Unnecessary mutability | Default to val, use var only when needed |
| Copying Java style | Verbose, un-idiomatic | Use data classes, extension functions, scope functions |
Ignoring data class |
Manual equals/hashCode for value objects |
Use data class for POJOs |
| Blocking threads in coroutines | Defeats concurrency, causes deadlocks | Use suspend + Dispatchers.IO for blocking calls |
Not using sealed class |
Incomplete when branches |
Use sealed classes for exhaustive hierarchies |
List vs MutableList confusion |
Mutating read-only lists (compile error) | Use mutableListOf() when mutation is needed |
| Skipping coroutines | Callback hell or blocking threads | Learn coroutines early — they're central to Kotlin |
Kotlin vs Java: Key Differences
// Java
String name = null;
if (name != null) {
System.out.println(name.toUpperCase());
}
// Kotlin
val name: String? = null
println(name?.uppercase()) // null — safe, no NPE
// Java
Person person = new Person();
person.setName("Alice");
person.setAge(30);
// Kotlin
val person = Person(name = "Alice", age = 30)
// or with apply:
val person2 = Person().apply { name = "Alice"; age = 30 }
// Java
List<String> names = Arrays.asList("a", "b", "c");
List<String> upper = new ArrayList<>();
for (String s : names) {
upper.add(s.toUpperCase());
}
// Kotlin
val names = listOf("a", "b", "c")
val upper = names.map { it.uppercase() }
Quick Reference
// Variables
val x = 10 // immutable
var y = 20 // mutable
val name: String = "hi" // explicit type
// Null safety
val s: String? = null
val len = s?.length ?: 0
val forced = s!!.length // throws if null
// String templates
"Hello, $name — length ${name.length}"
// Functions
fun add(a: Int, b: Int) = a + b
val greet = { name: String -> "Hello, $name" }
// Collections
val list = listOf(1, 2, 3)
val map = mapOf("a" to 1, "b" to 2)
val set = setOf(1, 2, 3)
val mList = mutableListOf<Int>()
// Data class
data class Point(val x: Int, val y: Int)
val p = Point(1, 2)
val p2 = p.copy(y = 10)
// Sealed class
sealed class State
object Loading : State()
data class Success(val data: String) : State()
data class Error(val msg: String) : State()
// Coroutines
suspend fun fetch(): String { delay(100); return "data" }
runBlocking { println(fetch()) }
6 FAQ
Q: Should I learn Java before Kotlin? Not required. Kotlin is a great first JVM language. However, understanding Java helps when working with Java libraries (which Kotlin interops with seamlessly), so some Java exposure is beneficial over time.
Q: Is Kotlin only for Android? No. Kotlin is used for server-side development (Spring Boot, Ktor), Kotlin Multiplatform (iOS + Android + web from shared code), data engineering, and CLI tools. The fastest-growing area is backend and KMP.
Q: How does Kotlin compare to Swift? Both are modern, null-safe languages — Kotlin for Android/JVM, Swift for iOS/macOS. The syntax is similar in many ways. If you know one, learning the other is much easier. Kotlin Multiplatform lets you share business logic between the two.
Q: What's Kotlin Multiplatform? KMP lets you write shared business logic (networking, data models, repositories) in Kotlin and use it on Android, iOS (via Kotlin/Native), web (via Kotlin/JS), and server. Each platform still uses its native UI framework (Compose, SwiftUI, React). Gaining adoption at major companies (Netflix, VMware, Philips).
Q: How hard are coroutines to learn?
The basic patterns (launch, async, delay) are easy — much simpler than Java threads. The advanced patterns (Flow, channels, structured concurrency) take a few weeks to internalize. Start with simple suspend functions and work up.
Q: Is Kotlin a good first programming language? Yes, especially if you're interested in Android development. Its null safety catches real bugs that beginners make constantly, and the syntax is clean and readable. Python might be slightly easier for pure beginners, but Kotlin produces production-ready applications from day one.