Cheatsheet Kotlin
Linguagem moderna para Android, backend e multiplataforma
Kotlin
Basic and Types
Hello World
fun main() {
println("Hello, World!")
}
// No mandatory class (unlike Java)
// main needs no args nor return typeThe entry point is the main function — it needs no wrapping class nor a declared return type. println writes a line to stdout. The compiler infers everything automatically.
Null Safety
var name: String? = null // nullable type
val len = name?.length // safe call (null if null)
val size = name?.length ?: 0 // elvis: default if null
val n = name!!.length // force (NPE if null)
// Runs only if non-null:
name?.let { println(it) }The type system eliminates NullPointerException at compile time. ? marks nullable types, ?. chains calls safely and ?: (elvis) provides defaults. let{} runs blocks only when the value exists.
Operators
// Arithmetic: + - * / % val r = 10 / 3 // 3 (integer) // Comparison and logical: == != > < >= <= && || ! // Range and membership: 1..10 // range 5 in 1..10 // true // Compound assignment: += -= *= /= %= // No ambiguous prefix ++/--
Integer division truncates. The in operator checks membership in ranges and collections. == compares by value (like equals in Java); === compares references. There are no automatic conversions in mixed expressions.
Variables (val and var)
val name = "Kotlin" // immutable (read-only) var age = 10 // mutable age = 11 // OK val pi: Double = 3.14 // explicit type // Type inference: val x = 42 // Int val s = "text" // String
Use val by default (immutable) to avoid bugs; only use var when the value needs to change. The type is inferred from the value, but can be annotated explicitly with : Type.
Arrays
val nums = arrayOf(1, 2, 3, 4, 5)
val zeros = IntArray(5) { 0 }
val evens = Array(5) { i -> i * 2 }
println(nums[0]) // 1
println(nums.size) // 5
nums[0] = 10 // mutable
// Iterate:
for (n in nums) println(n)Arrays have a fixed size after creation. IntArray/DoubleArray are optimized versions without boxing. The lambda in the constructor initializes each position. For dynamic collections prefer List/MutableList.
lateinit and lazy
// lateinit: late initialization (var)
class Activity {
lateinit var db: Database
fun onCreate() {
db = Database.open()
}
}
// lazy: computed on 1st access (val)
val config: Config by lazy {
Config.loadFromFile()
}lateinit declares non-null properties initialized later (e.g. dependency injection) — accessing them before throws an exception. by lazy defers the computation of a val until the first access (thread-safe by default).
Data Types
val integer: Int = 42 val longNum: Long = 100_000_000L val decimal: Double = 3.14 val flag: Boolean = true val letter: Char = 'A' val text: String = "Kotlin" val unit: Unit = Unit // Underscore for readability: val million = 1_000_000
Kotlin treats primitives the objects (no int/Integer distinction like Java). Underscores in numbers improve readability. Unit is the equivalent of Java's void but is a real type.
Type Aliases
typealias UserId = Long
typealias Callback = (String) -> Unit
typealias Table = Map<String, List<Any>>
fun register(id: UserId, cb: Callback) {
cb("user $id registered")
}
register(42L) { println(it) }typealias creates readable names for complex types — great for callbacks, IDs and nested maps. It does not create a new type (it is just a shortcut), but it greatly improves the readability of signatures.
Nothing and Any
// Any: super-type of everything
val obj: Any = 42
val obj2: Any = "text"
// Nothing: function never returns
fun fail(msg: String): Nothing {
throw IllegalStateException(msg)
}
// Useful in expressions:
val x = value ?: fail("no value")
// here x is non-nullAny is the super-type of all types (like Object in Java). Nothing indicates a function never returns normally (throws an exception or loops forever) — it helps the compiler infer nullability after ?:.
Strings and Templates
val name = "World"
val greeting = "Hello, $name!"
val template = "2 + 2 = ${2 + 2}"
// Multiline (triple-quoted):
val multi = """
|Line 1
|Line 2
""".trimMargin()
// No quote escaping:
val json = """{"name": "$name"}"""String templates with $ avoid concatenation; for expressions use ${}. Triple-quoted strings (\"\"\") preserve formatting and are ideal for SQL/JSON — trimMargin() removes the indentation.
Type Conversions
val x = 42 val s = x.toString() // Int -> String val n = "123".toInt() // String -> Int val d = x.toDouble() // Int -> Double val l = x.toLong() // Int -> Long // No implicit conversions: // val d: Double = x // ERROR! val d2: Double = x.toDouble() // toIntOrNull (safe): val inv = "abc".toIntOrNull() // null
Kotlin does no implicit numeric conversions — use toInt(), toDouble(), etc. explicitly. toIntOrNull() returns null instead of throwing an exception if the string is not a valid number.
Flow Control
If the an Expression
// if returns a value:
val max = if (a > b) a else b
// With blocks:
val msg = if (grade >= 10) {
println("Passed")
"OK"
} else {
"Failed"
}
// There is no ternary operator ?:
// Use if-else the an expressionIn Kotlin, if is an expression that returns a value — it replaces the ternary operator (which does not exist). The last statement of each block is the returned value. It makes code more declarative.
While and do-while
var n = 10
while (n > 0) {
n--
}
// do-while runs at least once:
do {
val input = readLine()
println("Read: $input")
} while (input != "quit")while checks the condition before running (may never run). do-while runs at least once — ideal for menus and input validation where you need to read before checking.
Smart Casts
fun process(obj: Any) {
if (obj is String) {
// obj is already String here:
println(obj.length)
println(obj.uppercase())
}
if (obj !is List<*>) return
// obj is List from here on:
println(obj.size)
}The compiler does automatic smart cast after is/!is checks — inside the block, the variable is treated the the tested type without a manual cast. It only works with local vals or immutable properties.
When
val result = when (x) {
1 -> "one"
2, 3 -> "two or three"
in 4..10 -> "between 4 and 10"
!in 20..30 -> "outside 20-30"
else -> "other"
}
// when is an expression (returns a value)
// else is mandatory if not exhaustivewhen replaces Java's switch with more powerful patterns: multiple comma-separated values, ranges with in, negation with !in. It is an expression that returns a value.
Break, Continue and Labels
for (i in 1..10) {
if (i == 3) continue // skips 3
if (i == 7) break // stops at 7
print(i) // 1 2 4 5 6
}
// Labels for nested loops:
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i * j > 4) break@outer
}
}continue jumps to the next iteration, break ends the loop. Labels (outer@) allow controlling nested loops — break@outer exits the outer loop directly.
return, break in Lambdas
fun search(list: List<Int>) {
list.forEach {
if (it == 3) return // returns from the function!
}
}
// Label to return only from the lambda:
list.forEach loop@{
if (it == 3) return@loop // only from this lambda
println(it)
}
// Implicit lambda name:
list.forEach {
if (it > 5) return@forEach
}A return inside a lambda returns from the enclosing function (non-local return). To return only from the lambda use labels (return@forEach). This is essential for controlling flow in higher-order functions.
Advanced When
// Without argument (if-else chain):
val msg = when {
x < 0 -> "negative"
x == 0 -> "zero"
else -> "positive"
}
// With types (smart cast):
fun type(obj: Any) = when (obj) {
is String -> "Text: ${obj.length}"
is Int -> "Number: $obj"
is List<*> -> "List: ${obj.size}"
else -> "?"
}when without an argument works the a more readable if-else chain. With is, Kotlin does automatic smart cast — inside the branch, obj is already treated the the tested type without a manual cast.
Ranges and Progressions
val r1 = 1..10 // IntRange (inclusive)
val r2 = 1 until 10 // 1..9 (excludes last)
val r3 = 'a'..'z' // CharRange
val r4 = 10 downTo 1 // descending
if (5 in r1) println("contains")
for (c in 'a'..'f') print(c) // abcdef
val list = (1..5).toList() // [1,2,3,4,5]Ranges are Iterable objects supporting iteration and membership checks (in). until excludes the last element (useful for indexes). They work with Int, Long and Char.
For Loops and Ranges
for (i in 1..5) print(i) // 1 2 3 4 5
for (i in 5 downTo 1) print(i) // 5 4 3 2 1
for (i in 0..10 step 2) print(i) // 0 2 4 6 8 10
for (i in 1 until 5) print(i) // 1 2 3 4
// With index and value:
for ((i, v) in list.withIndex()) {
println("$i: $v")
}Ranges (..) create inclusive sequences. downTo reverses the direction, step sets the increment and until excludes the last one. withIndex() gives access to the index and value simultaneously.
Expressions (try/when)
// try the an expression:
val n = try {
"42".toInt()
} catch (e: NumberFormatException) {
0
}
// when the an expression:
val code = when (status) {
"ok" -> 200
else -> 500
}
// if the an expression:
val max = if (a > b) a else bUnlike Java, if/when/try are expressions that return values in Kotlin. This eliminates temporary variables and makes the code more declarative. The last statement of each block is the value.
Functions and Lambdas
Function Declaration
fun sum(a: Int, b: Int): Int {
return a + b
}
// Expression body (inferred return):
fun double(x: Int) = x * 2
// No useful return (Unit):
fun greet(name: String) {
println("Hello, $name!")
}Functions with an expression body (=) are more concise and the return type is inferred. Unit (no return) can be omitted. Parameter types are always mandatory.
Extension Functions
fun String.capitalize(): String =
replaceFirstChar { it.uppercase() }
fun Int.isEven(): Boolean = this % 2 == 0
fun List<Int>.second(): Int? =
if (size >= 2) this[1] else null
// Usage:
"kotlin".capitalize() // "Kotlin"
4.isEven() // true
listOf(1, 2).second() // 2Extension functions add methods to existing classes without inheritance or modifying the source code. They are resolved statically at compile time. Ideal for utilities and more expressive APIs via fun Type.method().
Operator Overloading
data class Vec(val x: Int, val y: Int)
operator fun Vec.plus(o: Vec) =
Vec(x + o.x, y + o.y)
operator fun Vec.times(n: Int) =
Vec(x * n, y * n)
val v = Vec(1, 2) + Vec(3, 4) // Vec(4, 6)
val d = Vec(1, 2) * 3 // Vec(3, 6)Operator overloading allows using +, -, *, [] with custom types. Just mark the function with operator and use the conventional name (plus, times, get).
Default and Named Args
fun greet(name: String, prefix: String = "Hello") =
"$prefix, $name!"
greet("Anna") // "Hello, Anna!"
greet("Anna", "Good morning") // "Good morning, Anna!"
// Named arguments:
fun config(host: String, port: Int = 80, ssl: Boolean = false) {}
config("localhost", ssl = true) // port default 80Parameters with defaults eliminate overloads — the same function serves several calls. Named args improve readability and allow skipping parameters with defaults, specifying only the needed ones.
Higher-Order Functions
// Takes a function:
fun operation(a: Int, b: Int, op: (Int, Int) -> Int): Int {
return op(a, b)
}
operation(3, 4) { x, y -> x + y } // 7
// Returns a function:
fun multiplier(factor: Int): (Int) -> Int =
{ n -> n * factor }
val triple = multiplier(3)
triple(5) // 15Higher-order functions take or return other functions — the base of functional programming. The type (Int, Int) -> Int describes the signature. They allow creating factories, strategies and behavior composition.
Varargs and Spread
// vararg: N arguments of the same type
fun sum(vararg nums: Int) = nums.sum()
sum(1, 2, 3, 4, 5) // 15
// Spread operator (*):
val arr = intArrayOf(1, 2, 3)
sum(*arr) // 6
// vararg with other parameters:
fun log(level: String, vararg msgs: String) {
msgs.forEach { println("[$level] $it") }
}
log("INFO", "started", "ready")vararg accepts N arguments of the same type (internally an Array). The spread operator (*) passes an array the a vararg. It can be combined with normal parameters using named args.
Lambdas
val sum = { a: Int, b: Int -> a + b }
val double: (Int) -> Int = { it * 2 }
val nothing: () -> Unit = { println("hi") }
// it: implicit name (1 parameter)
listOf(1, 2, 3).forEach { println(it) }
// Multiple parameters:
val mult = { x: Int, y: Int -> x * y }
mult(3, 4) // 12Lambdas are first-class anonymous functions. it is the implicit name when there is a single parameter. The type (Int) -> Int describes the signature. They are the base of collection operations and DSLs.
Scope Functions
// let: transform + null-safety
val len = name?.let { it.trim().length }
// apply: configure (returns itself)
val p = Person().apply { name = "Anna"; age = 25 }
// run: block with a result
val info = p.run { "$name is $age" }
// also: side effect
list.also { println("Size: ${it.size}") }
// with: operations on the object
with(p) { println(name); println(age) }Scope functions eliminate temporary variables. let for null-safe transformations, apply for configuration/builders, run for computation, also for side effects and with for multiple operations on the same object.
Infix and Suspend Functions
// infix: call without dot nor parentheses
infix fun Int.plus2(x: Int) = this + x
val r = 5 plus2 3 // 8
// "to" is infix (creates Pair):
val pair = "a" to 1 // Pair("a", 1)
// suspend: coroutine function
suspend fun fetchData(): String {
delay(1000)
return "data"
}infix functions (1 parameter, member or extension) can be called without dot or parentheses — to is the most common example. suspend marks functions that can be suspended in coroutines (only callable from another suspend or coroutine).
Trailing Lambda
// Lambda the the last argument:
listOf(1, 2, 3).forEach({ println(it) })
// Trailing lambda (outside the parentheses):
listOf(1, 2, 3).forEach { println(it) }
// If it is the only argument:
listOf(1, 2, 3).map { it * 2 }
// Several lambdas:
fun build(init: Builder.() -> Unit, validate: () -> Boolean) {}
build({ /* config */ }) { /* validates */ }When the lambda is the last argument, it can go outside the parentheses (trailing lambda) — essential for fluent APIs and DSLs. If it is the only argument, the parentheses can be omitted entirely.
Inline Functions
// inline copies the lambda into the call site:
inline fun measureTime(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
println("${System.currentTimeMillis() - start}ms")
}
// noinline: do not inline this lambda
inline fun foo(a: () -> Unit, noinline b: () -> Unit) {}
// crossinline: forbids non-local return
inline fun bar(crossinline b: () -> Unit) {}inline copies the lambda body into the call site, eliminating object allocation — crucial in loops. noinline excludes a specific lambda; crossinline forbids non-local returns when the lambda is called in another context.
Classes and Objects
Classes and Constructors
class Person(val name: String, var age: Int) {
var email: String = ""
init {
require(age >= 0) { "Invalid age" }
}
// Secondary constructor:
constructor(name: String) : this(name, 0)
}
val p = Person("Anna", 25)The primary constructor in the class declaration is concise — val/var create properties automatically. The init block validates on creation. Secondary constructors delegate to the primary one with this().
Interfaces
interface Swimmer {
fun swim(): String
// Method with default implementation:
fun float(): String = "floating"
}
interface Flyer {
fun fly(): String
}
class Duck : Swimmer, Flyer {
override fun swim() = "swimming"
override fun fly() = "flying"
}Interfaces can have abstract methods and methods with default implementations. A class can implement multiple interfaces (but inherit from only one class). If defaults conflict, use super<Interface>.
Companion Object
class User private constructor(val name: String) {
companion object Factory {
fun create(json: String): User = User(json)
fun guest() = User("Guest")
const val MAX = 100
}
}
// Access via the class name:
val u = User.create("{...}")
val g = User.guest()
println(User.MAX)companion object replaces Java's static members — accessed via ClassName.member(). Ideal for factories, constants (const val) and utility methods. It can be named or anonymous.
Properties
class Rectangle(val width: Int, val height: Int) {
// Computed property:
val area: Int
get() = width * height
// With a custom setter:
var name: String = ""
set(value) {
field = value.trim()
}
}
val r = Rectangle(3, 4)
println(r.area) // 12 (computed)Properties can have a custom get() (computed) and a set() with logic. field is the backing field (the real value). There are no boilerplate getter/setter methods like Java — everything is a property.
Sealed Classes
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val msg: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun handle(r: Result<String>) = when (r) {
is Result.Success -> "Data: ${r.data}"
is Result.Error -> "Error: ${r.msg}"
Result.Loading -> "..."
} // exhaustive — no elsesealed class restricts the hierarchy to a fixed set of subclasses. The compiler checks exhaustiveness in when — if you add a new case, all whens break at compile time. Ideal for states and results.
Delegation (by)
interface Printer { fun print(msg: String) }
class ConsolePrinter : Printer {
override fun print(msg: String) = println(msg)
}
// Delegates the whole interface:
class Logger(printer: Printer) : Printer by printer
val log = Logger(ConsolePrinter())
log.print("hello") // prints to the consoleDelegation (by) implements the delegation pattern without boilerplate — the class forwards all interface calls to the delegated object. You can override specific methods if needed.
Data Classes
data class User(val name: String, val email: String, val age: Int)
val u1 = User("Anna", "ana@mail.com", 25)
val u2 = u1.copy(name = "Mary") // new object
println(u1) // User(name=Anna, ...)
println(u1 == u2) // false (compares values)
// Destructuring:
val (name, email) = u1data class automatically generates equals(), hashCode(), toString(), copy() and componentN(). Ideal for models, DTOs and states. copy() "modifies" immutables by creating a copy with changed fields.
Enum Classes
enum class State(val color: String) {
ACTIVE("#00FF00"),
INACTIVE("#FF0000"),
PENDING("#FFAA00");
fun description() = "State: $name ($color)"
}
val e = State.ACTIVE
println(e.color) // #00FF00
println(e.ordinal) // 0
val all = State.values()Enums in Kotlin are full classes — they can have properties, methods and implement interfaces. Each constant is an instance accessed via State.ACTIVE. Useful for states and fixed sets; values() lists them all.
Nested and Inner Classes
class Outer {
val x = 10
// Nested (no reference to the outer):
class Nested {
fun hello() = "nested"
}
// Inner (accesses the outer):
inner class Inner {
fun hello() = "x = $x"
}
}
val n = Outer.Nested()
val i = Outer().Inner()Nested classes have no reference to the outer instance (like static in Java). inner classes keep the reference and access the outer members. Use inner only when you really need the outer context.
Inheritance (open)
open class Animal(val name: String) {
open fun sound(): String = "..."
}
class Duck(name: String) : Animal(name) {
override fun sound() = "Quack!"
}
// super:
class Dog(name: String) : Animal(name) {
override fun sound() = "Woof! " + super.sound()
}Classes in Kotlin are final by default — use open to allow inheritance. Methods also need open to be overridden with override. super calls the superclass implementation.
Object (Singleton)
// Thread-safe singleton:
object AppConfig {
val apiUrl = "https://api.example.com"
fun getVersion() = "1.0.0"
}
// Usage:
println(AppConfig.apiUrl)
// Object expression (anonymous):
val listener = object : OnClickListener {
override fun onClick() = println("clicked")
}object creates thread-safe singletons with lazy initialization (a single instance). It replaces utility classes with static methods. Object expressions create anonymous instances of interfaces (like anonymous classes in Java).
Collections
Lists
val immutable = listOf(1, 2, 3, 4, 5) val mutable = mutableListOf(1, 2, 3) mutable.add(4) mutable.removeAt(0) mutable[0] = 10 println(immutable.first()) // 1 println(immutable.last()) // 5 println(immutable.size) // 5
listOf() creates immutable (read-only) lists — the API exposes no modification methods. mutableListOf() allows add/remove/set. Prefer immutables by default to avoid side effects.
Sorting
val nums = listOf(5, 2, 8, 1, 9)
val asc = nums.sorted() // [1,2,5,8,9]
val desc = nums.sortedDescending() // [9,8,5,2,1]
// By property:
val byAge = people.sortedBy { it.age }
val byName = people.sortedByDescending { it.name }
// Custom comparator:
val custom = nums.sortedWith(compareBy({ it % 2 }, { it }))sorted() returns a new sorted list (does not modify the original). sortedBy sorts by an extracted property. sortedWith takes a custom Comparator. compareBy composes multiple criteria.
flatMap and flatten
val lists = listOf(listOf(1, 2), listOf(3, 4), listOf(5))
val flat = lists.flatten() // [1,2,3,4,5]
val flat2 = lists.flatMap { it } // [1,2,3,4,5]
// Transform and flatten:
val words = sentences.flatMap { it.split(" ") }
// Unique words from all sentences:
val unique = sentences
.flatMap { it.split(" ") }
.distinct()flatMap transforms each element into a collection and flattens everything into a single list. flatten() flattens nested lists directly. Ideal for extracting and combining elements from nested structures.
Map and Set
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
println(map["a"]) // 1
println(map.getOrDefault("z", 0)) // 0
val mutMap = mutableMapOf<String, Int>()
mutMap["d"] = 4
val set = setOf(1, 2, 2, 3, 3) // {1, 2, 3}
println(3 in set) // trueMap stores key-value pairs with O(1) access by key. Set guarantees unique elements — duplicates are ignored. Both have mutable versions. to creates the Pair used when building maps.
Searching
val nums = listOf(5, 2, 8, 1, 9)
val found = nums.find { it > 7 } // 8 (first)
val last = nums.findLast { it < 5 } // 1
val index = nums.indexOf(8) // 2
val exists = nums.any { it > 8 } // true
val all = nums.all { it > 0 } // true
val none = nums.none { it < 0 } // true
val count = nums.count { it % 2 == 0 } // 2find returns the first element satisfying the predicate (or null). any/all/none check boolean conditions. count counts the ones that pass. indexOf gives the position of a value.
distinct, chunked and zip
val unique = listOf(1, 1, 2, 3, 3).distinct() // [1,2,3]
val groups = (1..7).chunked(3)
// [[1,2,3], [4,5,6], [7]]
val a = listOf("a", "b", "c")
val b = listOf(1, 2, 3)
val pairs = a.zip(b) // [(a,1), (b,2), (c,3)]
val (x, y) = listOf(1, 2, 3).partition { it % 2 == 0 }
// x=[2], y=[1,3]distinct() removes duplicates. chunked(n) splits into fixed-size sublists (useful for pagination). zip combines two lists into pairs. partition splits into two lists (pass/fail the predicate).
map and filter
val nums = listOf(1, 2, 3, 4, 5, 6)
val doubles = nums.map { it * 2 }
// [2, 4, 6, 8, 10, 12]
val evens = nums.filter { it % 2 == 0 }
// [2, 4, 6]
val names = people.map { it.name }
val adults = people.filter { it.age >= 18 }map transforms each element keeping the size; filter selects the ones passing the predicate. Both return new lists (they do not modify the original). They are the most used collection operations.
Grouping
val people = listOf(
Person("Anna", 25), Person("Ray", 30), Person("Mia", 25)
)
val byAge = people.groupBy { it.age }
// {25=[Anna,Mia], 30=[Ray]}
val names = people.associateBy { it.name }
// {"Anna"=Person(...), ...}
val pairs = people.associate { it.name to it.age }
// {"Anna"=25, "Ray"=30, "Mia"=25}groupBy groups elements by key into a Map<K, List<V>>. associateBy creates a Map indexed by key. associate builds arbitrary key-value pairs. Essential for turning lists into maps.
joinToString and Text Operations
val nums = listOf(1, 2, 3, 4, 5)
val csv = nums.joinToString(", ")
// "1, 2, 3, 4, 5"
val custom = nums.joinToString(
separator = " | ",
prefix = "[",
postfix = "]"
) { it.toString() }
// "[1 | 2 | 3 | 4 | 5]"
val words = "a b c".split(" ") // [a, b, c]
val joined = words.joinToString("") // "abc"joinToString() converts collections into a string with customizable separator, prefix and postfix, plus a per-element transformer. split() does the opposite. Ideal for building CSVs, logs and formatted output.
reduce, fold and sum
val nums = listOf(1, 2, 3, 4, 5)
val sum = nums.reduce { acc, n -> acc + n } // 15
val total = nums.sum() // 15
// fold: with initial value (different types):
val text = nums.fold("") { acc, n -> "$acc$n " }
val product = nums.fold(1) { acc, n -> acc * n } // 120reduce accumulates all elements into a single value (the first is the initial one). fold takes an explicit initial value and allows different types between accumulator and elements. sum() is a shortcut for adding numbers.
Sequences (Lazy)
// Eager: creates intermediate lists
val r1 = list.filter { it > 5 }.map { it * 2 }.take(3)
// Lazy: processes element by element
val r2 = list.asSequence()
.filter { it > 5 }
.map { it * 2 }
.take(3)
.toList() // only processes here
// Infinite:
val naturals = generateSequence(1) { it + 1 }
val first = naturals.take(10).toList()Sequences process element by element (lazy) instead of creating intermediate lists on every operation. Essential for large or infinite collections — it only computes what is needed when the terminal operation (toList()) is called.
Advanced
Coroutines Basics
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000) // suspends without blocking
println("World")
}
println("Hello")
}
// Hello (immediate) -> World (after 1s)Coroutines are suspendable functions that enable concurrency without blocked threads. launch starts a fire-and-forget coroutine. delay suspends without blocking the thread — ideal for I/O, timers and networking.
Exceptions in Coroutines
val handler = CoroutineExceptionHandler { _, e ->
println("Error: ${e.message}")
}
CoroutineScope(Dispatchers.Default).launch(handler) {
throw RuntimeException("failed")
}
// try/catch with async:
val r = async { operation() }
try {
r.await()
} catch (e: Exception) {
println("error: $e")
}Errors in launch propagate to the exception handler (CoroutineExceptionHandler). In async, the exception is rethrown at await() — use try/catch. A CoroutineScope cancels its children on failure (structured concurrency).
Reflection
import kotlin.reflect.full.*
class Person(val name: String, val age: Int)
val p = Person("Anna", 25)
val klass = p::class
println(klass.simpleName) // "Person"
klass.memberProperties.forEach {
println("${it.name} = ${it.get(p)}")
}
// Function reference:
val fn = ::println
fn("hello")Reflection (with kotlin-reflect) inspects classes, properties and functions at runtime. ::class gets the KClass, memberProperties lists the properties. Function references (::println) pass functions the values.
async / await
fun main() = runBlocking {
val r1 = async { fetchData1() }
val r2 = async { fetchData2() }
// Wait for both in parallel:
val total = r1.await() + r2.await()
println("Total: $total")
}
suspend fun fetchData1(): Int {
delay(1000); return 42
}async starts a coroutine that returns a Deferred (future). await() suspends until the result is available. Multiple async run in parallel — ideal for concurrent network calls that reduce total time.
DSL Building
class HtmlBuilder {
val elements = mutableListOf<String>()
fun div(c: String) { elements.add("<div>$c</div>") }
fun p(c: String) { elements.add("<p>$c</p>") }
}
fun html(block: HtmlBuilder.() -> Unit): String {
val b = HtmlBuilder().apply(block)
return b.elements.joinToString("\n")
}
val page = html {
div("Title")
p("Content")
}DSLs in Kotlin use extension lambdas (Receiver.() -> Unit) to create declarative APIs. The receiver object becomes the implicit this inside the block. Used in Gradle, Ktor, Exposed and Jetpack Compose.
Structured Concurrency
fun main() = runBlocking {
// coroutineScope: waits for all children
coroutineScope {
launch { task1() }
launch { task2() }
} // only continues when both finish
// supervisorScope: a failure does not cancel siblings
supervisorScope {
val a = launch { throw Exception() }
val b = launch { task2() } // continues
}
}Structured concurrency guarantees coroutines have a defined lifecycle. coroutineScope waits for all children and propagates failures. supervisorScope isolates failures — one child failing does not cancel its siblings. Essential for robust code.
Dispatchers
import kotlinx.coroutines.*
fun main() = runBlocking {
// CPU-bound:
val r = withContext(Dispatchers.Default) {
heavyComputation()
}
// I/O (network, disk):
val data = withContext(Dispatchers.IO) {
readFile()
}
// UI (Android):
withContext(Dispatchers.Main) { updateUi() }
}Dispatchers define which thread pool the coroutine runs on. Dispatchers.Default for CPU-bound work, Dispatchers.IO for I/O (network/disk), Dispatchers.Main for the UI (Android). withContext switches dispatcher without creating a new coroutine.
Delegates (lazy/observable)
// lazy: computed once on first access
val config: Config by lazy {
Config.loadFromFile()
}
// observable: notifies changes
var name: String by Delegates.observable("") { _, old, new ->
println("$old -> $new")
}
// vetoable: can reject
var age: Int by Delegates.vetoable(0) { _, _, new ->
new >= 0
}by lazy defers an expensive computation until the first access (thread-safe singleton). observable notifies after each change. vetoable allows rejecting invalid values before applying. All eliminate getter/setter boilerplate.
Channel (Communication)
import kotlinx.coroutines.channels.Channel
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (i in 1..5) channel.send(i * i)
channel.close()
}
for (value in channel) {
println(value) // 1, 4, 9, 16, 25
}
}Channel enables communication between coroutines (producer/consumer). send() sends (suspends if full), receive() receives (suspends if empty). close() ends the channel. Iterating over the channel receives until it closes.
Flow (Reactive)
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
runBlocking {
numbers()
.filter { it % 2 == 0 }
.map { it * 10 }
.collect { println(it) } // 20, 40
}Flow is the reactive API of Coroutines — it emits multiple values asynchronously. It is cold (only runs when collect is called). Supports operators like filter, map, take — similar to RxJava but built in.
Annotations
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Log(val level: String = "INFO")
@Log(level = "DEBUG")
fun operation() { }
// Stdlib annotations:
@Deprecated("use newFn()", ReplaceWith("newFn()"))
fun oldFn() {}
@JvmStatic
fun utility() {}Annotations add metadata to code. @Target defines where it applies, @Retention whether it is available at runtime. The stdlib has @Deprecated (with a replacement suggestion) and @JvmStatic/@JvmOverloads for Java interop.
OO and Generics
Visibility Modifiers
class Example {
public val a = 1 // public (default)
private val b = 2 // only in this class
protected val c = 3 // class + subclasses
internal val d = 4 // only in this module
}
// Top-level:
private fun helper() {} // only in this file
internal val config = 1 // only in this moduleKotlin has four modifiers: public (default), private (class/file), protected (class + subclasses) and internal (visible in the module). Everything is public by default — restrict with private the much the possible.
reified and inline
// reified keeps the type at runtime:
inline fun <reified T> typeOf(x: Any): Boolean =
x is T
typeOf<String>("hello") // true
typeOf<Int>("hello") // false
// Practical use:
inline fun <reified T> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)Normally generic types are erased at runtime (type erasure). reified (only with inline) preserves the type, allowing is T checks and getting T::class. Widely used in serialization and reflection.
Value Classes (inline)
@JvmInline
value class Email(val value: String) {
init { require("@" in value) { "invalid email" } }
fun domain() = value.substringAfter("@")
}
val e = Email("ana@mail.com")
println(e.domain()) // mail.com
// At runtime it is just a String (no object overhead)value class (inline class) wraps a single value with no object overhead at runtime — type safety of a distinct type with the performance of a primitive. Ideal for IDs, emails and newtypes with validation in init.
Abstract Classes
abstract class Shape {
abstract fun area(): Double
// Concrete method:
fun describe() = "Area: ${area()}"
}
class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
}
// Cannot instantiate:
// val f = Shape() // ERRORabstract classes cannot be instantiated and can have abstract methods (no body) that subclasses implement. They combine inheritance with a contract — useful when there is shared code but subtype-specific behavior.
Type Checks and Casts
fun process(obj: Any) {
if (obj is String) {
println(obj.uppercase()) // smart cast
}
// Explicit cast:
val s = obj the? String // safe (null if it fails)
val n = obj the Int // throws ClassCastException
// Star projection:
fun length(list: List<*>) = list.size
}is checks the type with automatic smart cast. the? is the safe cast (returns null if it fails); the throws an exception. List<*> (star projection) accepts lists of any type when the concrete type is not needed.
Builder Pattern
class OrderBuilder {
var item: String = ""
var quantity: Int = 1
var urgent: Boolean = false
fun build() = Order(item, quantity, urgent)
}
fun order(block: OrderBuilder.() -> Unit): Order =
OrderBuilder().apply(block).build()
val o = order {
item = "Book"
quantity = 2
urgent = true
}The builder pattern in Kotlin uses an extension lambda (Builder.() -> Unit) with apply — it creates a declarative DSL for configuring objects. More readable than constructors with many optional parameters.
Generics
// Generic class:
class Box<T>(val value: T)
val intBox = Box(42) // Box<Int> inferred
val strBox = Box("text")
// Generic function:
fun <T> first(list: List<T>): T? =
list.firstOrNull()
// Constraint:
fun <T : Comparable<T>> biggest(a: T, b: T): T =
if (a > b) a else bGenerics allow reusable code with type safety. The compiler infers types in most cases. Constraints (T : Comparable<T>) restrict accepted types to those implementing an interface.
equals and hashCode
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Point) return false
return x == other.x && y == other.y
}
override fun hashCode(): Int = 31 * x + y
}
// data class generates this automatically!
data class Point2(val x: Int, val y: Int)Override equals() and hashCode() together for value comparison (required in Set/Map). === compares references. Prefer data class, which generates both automatically.
Variance (in/out)
// out (covariance): only produces T
class Producer<out T>(val item: T) {
fun get(): T = item
}
val p: Producer<Any> = Producer("text") // OK
// in (contravariance): only consumes T
class Consumer<in T> {
fun consume(item: T) { println(item) }
}
val c: Consumer<String> = Consumer<Any>() // OKout (covariance) means the class only produces T — a Producer<String> is a Producer<Any>. in (contravariance) only consumes T. It allows safe subtyping in generics (PECS: Producer Extends, Consumer Super).
Sealed Interfaces
sealed interface Event {
data class Click(val x: Int, val y: Int) : Event
data class Key(val code: Int) : Event
object Close : Event
}
fun handle(e: Event) = when (e) {
is Event.Click -> "click at ${e.x},${e.y}"
is Event.Key -> "key ${e.code}"
Event.Close -> "close"
} // exhaustivesealed interface (Kotlin 1.5+) combines interfaces with a closed hierarchy — the implementations are known at compile time, allowing exhaustive when. More flexible than sealed class because a class can implement several.
Errors and Null Safety
Try / Catch
// try is an expression:
val n = try {
"42".toInt()
} catch (e: NumberFormatException) {
0
} finally {
println("attempt done")
}
// Multiple catch:
try {
operation()
} catch (e: IllegalArgumentException) {
println("argument: ${e.message}")
} catch (e: Exception) {
println("other: $e")
}try is an expression that returns a value — the last statement of the try or catch block is the result. The finally always runs. You can have multiple catch blocks per exception type.
Chained Safe Calls
// Safe chaining:
val city = person?.address?.city?.name
// Equivalent without safe call (verbose):
// if (person != null && person.address != null ...)
// With let (only runs if non-null):
person?.address?.let { addr ->
println(addr.city)
sendMail(addr)
}
// Combines with elvis:
val name = person?.name ?: "Unknown"Safe calls (?.) chain together — if any element is null, the result is null without a NullPointerException. Combined with let they run blocks only when the whole chain exists. Replaces nested null-checks.
The !! Operator (not-null)
var name: String? = "Kotlin"
// Forces non-null (dangerous):
val len = name!!.length
// If it is null -> NullPointerException
// name = null
// name!!.length // CRASH!
// Always prefer safe alternatives:
val a = name?.length ?: 0 // elvis
val b = name?.let { it.length } // letThe !! operator converts a nullable type to non-null, throwing NullPointerException if it is null. It is an "I know what I am doing" — avoid it. Prefer ?., ?: or let for truly null-safe code.
Throw and Exceptions
fun divide(a: Int, b: Int): Int {
if (b == 0) {
throw IllegalArgumentException("division by zero")
}
return a / b
}
// throw is an expression (type Nothing):
fun validate(x: Int): Int =
if (x < 0) throw IllegalArgumentException("negative")
else xthrow throws exceptions (all are Throwable). Kotlin has no checked exceptions — declaring or catching is not mandatory. throw is an expression of type Nothing, useful in if/elvis expressions.
require and check
// require: validates arguments (IllegalArgumentException)
fun createUser(name: String, age: Int) {
require(name.isNotBlank()) { "blank name" }
require(age in 0..150) { "invalid age: $age" }
}
// check: validates state (IllegalStateException)
fun start() {
check(!started) { "already started" }
started = true
}
// assert (debug only):
assert(x > 0) { "x must be positive" }require() validates function arguments (throws IllegalArgumentException). check() validates the object state (throws IllegalStateException). Both accept a lazy message. They make preconditions explicit and documented.
Platform Types
// Java code may return null: // String s = javaLib.getText(); // may be null // Kotlin treats it the a platform type (T!): val s = javaLib.getText() // String! // Compiles, but may throw NPE at runtime // Make it explicit: val safe: String? = javaLib.getText() val len = safe?.length ?: 0 // @Nullable/@NotNull in Java help
Platform types (T!) come from Java code without nullability annotations — Kotlin does not know whether they can be null. The responsibility is on the programmer. Annotate the String? explicitly and use safe calls when interacting with Java.
Custom Exceptions
class InsufficientBalanceException(
val balance: Double,
val amount: Double
) : Exception("Balance $balance insufficient for $amount")
fun withdraw(balance: Double, amount: Double) {
if (amount > balance) {
throw InsufficientBalanceException(balance, amount)
}
}
try {
withdraw(100.0, 150.0)
} catch (e: InsufficientBalanceException) {
println(e.balance)
}Create custom exceptions by extending Exception (recoverable) or RuntimeException. Add relevant properties (like balance) for context. Keep the exception hierarchy simple and specific.
Result Type
val result: Result<Int> = runCatching {
"42".toInt()
}
result.onSuccess { println("Ok: $it") }
result.onFailure { println("Error: $it") }
// Get the value:
val n = result.getOrDefault(0)
val n2 = result.getOrNull()
val n3 = result.getOrThrow()
// Chain:
val double = result.map { it * 2 }Result<T> encapsulates success or failure without throwing. runCatching{} captures exceptions into a Result. onSuccess/onFailure handle each case; map transforms the success. A functional alternative to try/catch.
Elvis Operator
// Default if null:
val len = name?.length ?: 0
// With throw:
fun getName(): String {
return input?.trim()
?: throw IllegalStateException("no name")
}
// Chained:
val value = a ?: b ?: c ?: "default"
// With return:
fun process(x: Int?) {
val n = x ?: return // early return if null
println(n * 2)
}The elvis operator (?:) provides an alternative value when the left side is null. It combines with throw (throws if null), return (early exit) and chaining. It is the central tool of idiomatic null-safety.
runCatching
// Instead of try/catch:
val value = runCatching {
api.fetchData()
}.getOrElse { error ->
println("Failure: $error")
localData()
}
// Transform the result:
val processed = runCatching { parse(input) }
.map { it.uppercase() }
.recover { "default" }
.getOrThrow()runCatching{} runs a block and captures any exception into a Result. getOrElse provides an alternative on failure; recover turns the error into a value. Enables functional pipelines without verbose try/catch.
Tools and Languages
Essential Idioms
// Singleton:
object Resource { }
// Immutable by default:
val list = listOf(1, 2, 3)
// Expressions everywhere:
val max = if (a > b) a else b
// Data class for models:
data class User(val name: String)
// Scope functions:
val s = StringBuilder().apply { append("hi") }Kotlin idioms: prefer val (immutable), use data class for models, expressions instead of statements, object for singletons and scope functions to eliminate temporary variables. Concise and expressive code.
Testing
import kotlin.test.*
class CalculatorTest {
@Test
fun `adds two numbers`() {
val r = sum(2, 3)
assertEquals(5, r)
}
@Test
fun `throws on division by zero`() {
assertFailsWith<IllegalArgumentException> {
divide(1, 0)
}
}
@BeforeTest fun setup() { }
@AfterTest fun cleanup() { }
}Kotlin Test (kotlin.test) provides multiplatform assertions. Tests can have names with spaces between backticks. assertEquals checks equality, assertFailsWith checks exceptions. @BeforeTest/@AfterTest do setup/cleanup.
Performance and Boxing
// Optimized primitives:
val arr = IntArray(1000) { it } // no boxing
val list = List(1000) { it } // Int? boxed
// inline avoids lambda allocation:
inline fun repeatN(n: Int, block: () -> Unit) {
for (i in 0 until n) block()
}
// Sequences for large collections:
val r = data.asSequence()
.filter { it > 0 }
.map { it * 2 }
.take(10)
.toList()Use IntArray/DoubleArray to avoid boxing. inline eliminates lambda object allocation in loops. Sequences avoid intermediate lists in operation chains. Prefer val and immutability for compiler optimizations.
Useful Stdlib
// Strings:
"hello".uppercase() // "HELLO"
"a,b,c".split(",") // [a, b, c]
" x ".trim() // "x"
// Numbers:
val r = (1..10).random()
val n = 3.14159.roundToInt() // 3
// Collections:
listOf(1, 2, 3).shuffled()
listOf(1, 2, 3).reversed()
"a" to 1 // PairThe Kotlin stdlib is rich: uppercase/split/trim for strings, random/roundToInt for numbers, shuffled/reversed for collections. to creates a Pair. It greatly reduces utility code.
Coroutines Test
import kotlinx.coroutines.test.*
@Test
fun asyncTest() = runTest {
// delay is skipped automatically:
val result = fetchData() // suspend
assertEquals("data", result)
}
@Test
fun concurrentTest() = runTest {
val r1 = async { operation1() }
val r2 = async { operation2() }
assertEquals(42, r1.await() + r2.await())
}kotlinx-coroutines-test provides runTest, which runs test coroutines deterministically — delay is skipped (virtual time), making tests fast. async/launch work normally inside the test.
Best Practices
// 1. val by default, var only if needed val total = calculate() // 2. Immutable: listOf instead of mutableListOf val items = listOf(1, 2, 3) // 3. Expressions instead of statements val r = if (x > 0) x else -x // 4. data class for models data class Dto(val id: Int, val name: String) // 5. Null-safety instead of !! val n = value?.length ?: 0
Best practices: val by default, immutable collections, expressions instead of statements, data class for models and idiomatic null-safety (never !!). Follow the official Kotlin Coding Conventions for consistent style.
Java Interop
// Kotlin calls Java directly:
val list = java.util.ArrayList<String>()
val date = java.time.LocalDate.now()
// Java calls Kotlin:
// (compiles to normal JVM bytecode)
// Interop annotations:
@JvmStatic fun util() {} // real static
@JvmOverloads fun f(a: Int, b: Int = 0) {}
@JvmField val x = 1 // public field
@file:JvmName("Utils") // file nameKotlin is 100% interoperable with Java — it calls Java libraries directly and vice versa. The @JvmStatic, @JvmOverloads, @JvmField and @JvmName annotations control how Kotlin code appears to Java.
Multiplatform
// commonMain (shared):
expect fun platform(): String
// androidMain:
actual fun platform() = "Android"
// iosMain:
actual fun platform() = "iOS"
// jvmMain:
actual fun platform() = "JVM"
// Usage in common code:
fun greet() = "Hello from ${platform()}"Kotlin Multiplatform shares business logic across platforms (Android, iOS, JVM, JS, Native). expect declares an API in common code; actual provides each platform-specific implementation. Ideal for networking, models and validation.
Gradle Kotlin DSL
// build.gradle.kts:
plugins {
kotlin("jvm") version "1.9.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(17)
}The Gradle Kotlin DSL (build.gradle.kts) uses Kotlin instead of Groovy — type-safe, IDE autocompletion and refactorable. plugins{} applies plugins, dependencies{} manages dependencies. It is the recommended standard for new projects.
Null-safe Design
// Return non-null whenever possible:
fun name(): String = "Anna" // never null
// Use nullable only when needed:
fun find(id: Int): User? =
base.firstOrNull { it.id == id }
// Force handling at the call site:
val u = find(1)
?: throw NotFoundException("user 1")
// Non-null types + elvis = safe codeNull-safe design: return non-null types by default; use nullable (?) only when absence is a valid state. Force handling at the call site with ?: or let. The fewer nullables in the API, the safer the code.