Kotlin logoKotlinv2.4INTERMEDIATE

Kotlin

Kotlin cheat sheet covering syntax, null safety, coroutines, data classes, extensions, and Android development patterns with examples.

15 min read

Sign in to mark items as known and track your progress.

Sign in

Getting Started

Kotlin basics and fundamentals

Variables & Types

Variable declarations and basic types

javascript
// Immutable variable (read-only)
val name: String = "Kotlin"
val age = 25  // Type inference
// name = "Java"  // Error: Val cannot be reassigned

// Mutable variable
var count = 0
count++  // OK
count = 10  // OK

// Basic types
val byte: Byte = 127
val short: Short = 32767
val int: Int = 2147483647
val long: Long = 9223372036854775807L
val float: Float = 3.14f
val double: Double = 3.141592653589793
val boolean: Boolean = true
val char: Char = 'K'
val string: String = "Hello, Kotlin"
💡 Use val by default, var only when mutability is needed
⚡ Type inference makes explicit types often unnecessary
📌 const val for compile-time constants, val for runtime constants
🟢 lateinit for non-null properties initialized after construction

String Templates

String interpolation and manipulation

javascript
// String templates
val name = "Alice"
val age = 30
val message = "Hello, $name! You are $age years old."
val expression = "Next year you'll be ${age + 1}"

// Multiline strings
val multiline = """
    |First line
    |Second line
    |Third line
    """.trimMargin()

val rawString = """
    No escaping needed: 
 	 \
    Preserves formatting
"""

// String operations
val str = "Kotlin"
println(str.length)  // 6
println(str.uppercase())  // KOTLIN
println(str[0])  // K
println(str.substring(0, 3))  // Kot
💡 Use $ for simple variables, ${} for expressions
⚡ Triple quotes for multiline strings without escaping
📌 trimMargin() removes leading whitespace with | delimiter
🟢 buildString is efficient for complex string construction

Null Safety

Kotlin null safety system

Nullable Types

Working with nullable types safely

javascript
// Nullable types
var nullable: String? = "Hello"
nullable = null  // OK

var nonNull: String = "Hello"
// nonNull = null  // Error: Null cannot be assigned

// Safe call operator
val length = nullable?.length  // Returns null if nullable is null

// Elvis operator
val lengthOrZero = nullable?.length ?: 0
val message = nullable ?: "Default message"

// Not-null assertion (use sparingly!)
val forceLength = nullable!!.length  // Throws NPE if null

// Safe casts
val any: Any = "String"
val str: String? = any as? String  // Safe cast
val num: Int? = any as? Int  // Returns null
💡 ? makes a type nullable, safe call ?. prevents NPE
⚡ Elvis operator ?: provides default for null values
📌 Avoid !! (not-null assertion) - it defeats null safety
🟢 Smart casts eliminate need for explicit casting after null check

Null Safety Patterns

Advanced null handling patterns

javascript
// Scoped functions for null handling
val str: String? = "Hello"

// let - execute block if not null
str?.let {
    println("Length: ${it.length}")
}

// also - side effects
str?.also {
    println("Processing: $it")
}?.uppercase()

// run - execute block with receiver
val result = str?.run {
    println("Running on $this")
    length * 2
}

// takeIf / takeUnless
val email = "user@example.com"
val validEmail = email.takeIf { it.contains("@") }
val invalidEmail = email.takeUnless { it.contains("@") }
💡 Scoped functions (let, run, also) handle nullables elegantly
⚡ takeIf/takeUnless return value or null based on predicate
📌 Delegates.notNull() for properties initialized after construction
🟢 Contracts tell compiler about null state after function calls

Functions

Function declarations and features

Function Basics

Function syntax and parameters

javascript
// Basic function
fun greet(name: String): String {
    return "Hello, $name!"
}

// Single expression function
fun add(a: Int, b: Int) = a + b

// Default parameters
fun connect(host: String = "localhost", port: Int = 8080) {
    println("Connecting to $host:$port")
}

// Named arguments
connect(port = 3000, host = "example.com")

// Unit return type (void)
fun printMessage(msg: String): Unit {
    println(msg)
}
// Unit can be omitted
fun log(msg: String) {
    println("[LOG] $msg")
}
💡 Single expression functions use = instead of braces
⚡ Named arguments improve readability and allow skipping defaults
📌 tailrec optimizes tail recursive functions to loops
🟢 Infix functions allow natural DSL-like syntax

Higher-Order Functions

Functions as parameters and lambdas

javascript
// Lambda expressions
val sum = { x: Int, y: Int -> x + y }
val square: (Int) -> Int = { it * it }

// Higher-order function
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
    return operation(x, y)
}

val result = calculate(10, 5) { a, b -> a * b }

// Function with receiver
fun buildString(action: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.action()
    return sb.toString()
}

val html = buildString {
    append("<h1>")
    append("Title")
    append("</h1>")
}

// Inline functions
inline fun measureTime(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}
💡 Lambdas are functions defined with { } syntax
⚡ inline functions reduce overhead by inlining at call site
📌 crossinline prevents non-local returns in lambdas
🟢 Function with receiver enables DSL-style APIs

Classes & Objects

Object-oriented programming in Kotlin

Classes

Class declarations and constructors

javascript
// Primary constructor
class Person(val name: String, var age: Int)

// Full syntax with init block
class Student(firstName: String, lastName: String) {
    val fullName: String
    var grade: Int = 0

    init {
        fullName = "$firstName $lastName"
        println("Student created: $fullName")
    }

    // Secondary constructor
    constructor(name: String) : this(name, "") {
        println("Secondary constructor called")
    }
}

// Properties with getters/setters
class Temperature {
    var celsius: Double = 0.0
        get() = field
        set(value) {
            field = if (value < -273.15) -273.15 else value
        }

    val fahrenheit: Double
        get() = celsius * 9/5 + 32
}
💡 Primary constructor is part of class header
⚡ init blocks run during instance initialization
📌 Use field keyword to access backing field in setters
🟢 Class delegation with by keyword promotes composition

Inheritance

Class inheritance and polymorphism

javascript
// Open class (can be inherited)
open class Animal(val name: String) {
    open fun makeSound() {
        println("Some generic animal sound")
    }

    fun sleep() {
        println("$name is sleeping")
    }
}

// Inheritance
class Dog(name: String, val breed: String) : Animal(name) {
    override fun makeSound() {
        println("$name barks: Woof!")
    }

    fun wagTail() {
        println("$name is wagging tail")
    }
}

// Abstract classes
abstract class Shape {
    abstract val area: Double
    abstract fun draw()

    fun describe() {
        println("Area: $area")
    }
}

class Circle(private val radius: Double) : Shape() {
    override val area = Math.PI * radius * radius

    override fun draw() {
        println("Drawing circle with radius $radius")
    }
}
💡 Classes and members are final by default, use open to allow inheritance
⚡ Sealed classes enable exhaustive when expressions
📌 Interfaces can have default implementations
🟢 Use abstract classes for shared state, interfaces for contracts

Data Classes & Objects

Data classes, objects, and companions

Data Classes

Classes for holding data

javascript
// Data class
data class User(
    val id: Int,
    val name: String,
    var email: String
)

val user = User(1, "Alice", "alice@example.com")
val copy = user.copy(email = "newemail@example.com")

// Destructuring
val (id, name, email) = user

// Generated methods
println(user.toString())  // User(id=1, name=Alice, email=alice@example.com)
println(user.hashCode())
println(user == copy)  // false (different email)

// Data class requirements
// - Primary constructor with at least one parameter
// - All parameters marked as val or var
// - Cannot be abstract, open, sealed, or inner
💡 Data classes auto-generate equals(), hashCode(), toString(), copy()
⚡ Destructuring declarations use componentN() functions
📌 Only properties in primary constructor are used in generated methods
🟢 Use copy() to create modified instances immutably

Objects & Companions

Singleton objects and companion objects

javascript
// Object declaration (Singleton)
object DatabaseConnection {
    init {
        println("Initializing database connection")
    }

    fun connect() {
        println("Connecting to database...")
    }
}

// Usage
DatabaseConnection.connect()

// Companion object
class Factory {
    companion object {
        private var counter = 0

        fun create(): Factory {
            counter++
            return Factory()
        }

        fun getCount() = counter
    }
}

val instance = Factory.create()
val count = Factory.getCount()

// Named companion
class MyClass {
    companion object Loader {
        fun load() = MyClass()
    }
}
💡 object creates a singleton with thread-safe lazy initialization
⚡ Companion objects are per-class singletons for factory methods
📌 @JvmStatic makes companion methods static in Java
🟢 Object expressions create anonymous classes on the fly

Collections

Lists, Sets, Maps and operations

Collection Types

Lists, Sets, and Maps

javascript
// Immutable collections
val list = listOf(1, 2, 3)
val set = setOf("a", "b", "c")
val map = mapOf("key1" to "value1", "key2" to "value2")

// Mutable collections
val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4)
mutableList.removeAt(0)

val mutableSet = mutableSetOf("a", "b")
mutableSet.add("c")

val mutableMap = mutableMapOf<String, Int>()
mutableMap["one"] = 1
mutableMap["two"] = 2

// Array types
val intArray = intArrayOf(1, 2, 3)
val stringArray = arrayOf("a", "b", "c")
val nullArray = arrayOfNulls<String>(5)
💡 Default collections are immutable, use mutable* for mutability
⚡ to infix function creates Pair for map entries
📌 buildList/buildMap efficiently create immutable collections
🟢 Arrays have fixed size, Lists can be resized (if mutable)

Collection Operations

Transformations and aggregations

javascript
// Transformations
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
val filtered = numbers.filter { it > 2 }
val strings = numbers.map { "Item $it" }

// Aggregations
val sum = numbers.sum()
val average = numbers.average()
val max = numbers.maxOrNull()
val count = numbers.count { it % 2 == 0 }

// Grouping
val words = listOf("apple", "apricot", "banana", "blueberry")
val grouped = words.groupBy { it.first() }
// {a=[apple, apricot], b=[banana, blueberry]}

// Flattening
val nested = listOf(listOf(1, 2), listOf(3, 4))
val flat = nested.flatten()  // [1, 2, 3, 4]
val flatMapped = nested.flatMap { it.map { n -> n * 2 } }  // [2, 4, 6, 8]
💡 Collection operations return new collections (immutable)
⚡ Use sequences for lazy evaluation of large collections
📌 fold provides initial value, reduce uses first element
🟢 partition splits collection into two based on predicate

Control Flow

Conditionals and loops

Conditionals

if, when, and conditional expressions

javascript
// If expression
val max = if (a > b) a else b

// If-else chain
val result = if (score >= 90) {
    "A"
} else if (score >= 80) {
    "B"
} else if (score >= 70) {
    "C"
} else {
    "F"
}

// When expression (like switch)
when (x) {
    1 -> println("One")
    2 -> println("Two")
    3, 4 -> println("Three or Four")
    in 5..10 -> println("Between 5 and 10")
    !in 10..20 -> println("Not between 10 and 20")
    is String -> println("It's a string")
    else -> println("Unknown")
}

// When as expression
val description = when (val code = getCode()) {
    200 -> "OK"
    404 -> "Not Found"
    500 -> "Server Error"
    else -> "Unknown code: $code"
}
💡 if and when are expressions that return values
⚡ when without argument acts like if-else chain
📌 Sealed classes with when provide exhaustive checks
🟢 Smart casts work in when branches after type checks

Loops

for, while, and loop control

javascript
// For loops
for (i in 1..5) {
    println(i)  // 1, 2, 3, 4, 5
}

for (i in 1 until 5) {
    println(i)  // 1, 2, 3, 4
}

for (i in 5 downTo 1) {
    println(i)  // 5, 4, 3, 2, 1
}

for (i in 1..10 step 2) {
    println(i)  // 1, 3, 5, 7, 9
}

// Iterating collections
val list = listOf("a", "b", "c")
for (item in list) {
    println(item)
}

for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

// While loops
var x = 5
while (x > 0) {
    println(x--)
}

// Do-while
do {
    val y = readLine()
} while (y != "exit")
💡 Ranges (1..5) and progressions (step, downTo) control iteration
⚡ withIndex() provides index-value pairs for iteration
📌 Labels allow breaking/continuing outer loops
🟢 forEach is inline, so return exits the enclosing function

Extensions

Extension functions and properties

Extension Functions

Adding functions to existing types

javascript
// Extension function
fun String.removeSpaces(): String {
    return this.replace(" ", "")
}

val text = "Hello World"
println(text.removeSpaces())  // HelloWorld

// Extension properties
val String.lastChar: Char
    get() = this[length - 1]

println("Kotlin".lastChar)  // n

// Extensions on nullable types
fun String?.isNullOrEmpty(): Boolean {
    return this == null || this.isEmpty()
}

val nullString: String? = null
println(nullString.isNullOrEmpty())  // true

// Generic extensions
fun <T> List<T>.secondOrNull(): T? {
    return if (size >= 2) this[1] else null
}
💡 Extensions add functions without modifying original class
⚡ Extensions are resolved statically, not polymorphically
📌 Member functions always win over extensions
🟢 Can extend nullable types with null-safe operations

Scope Functions

let, run, with, apply, also

javascript
// let - it as argument, returns result
val result = "Hello".let {
    println("Original: $it")
    it.uppercase()
}  // HELLO

// Null safety with let
val nullableString: String? = "Kotlin"
nullableString?.let {
    println("Length: ${it.length}")
}

// run - this as receiver, returns result
val formatted = "hello".run {
    println("Original: $this")
    uppercase()
}  // HELLO

// with - non-extension, returns result
val numbers = mutableListOf(1, 2, 3)
val sum = with(numbers) {
    add(4)
    add(5)
    sum()
}

// apply - this as receiver, returns receiver
val person = Person().apply {
    name = "Alice"
    age = 30
    email = "alice@example.com"
}

// also - it as argument, returns receiver
val list = mutableListOf(1, 2, 3).also {
    println("Adding items to list: $it")
    it.add(4)
}
💡 let/also use it, run/apply/with use this
⚡ let/run/with return result, apply/also return receiver
📌 Use let for null checks and transformations
🟢 Use apply for object configuration, also for side effects

Coroutines

Asynchronous programming with coroutines

Coroutine Basics

Launching and managing coroutines

javascript
// Coroutine scope and launch
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000)
        println("World")
    }
    println("Hello")
}

// Async and await
suspend fun fetchUser(): User {
    delay(1000)  // Simulated network call
    return User("Alice")
}

suspend fun fetchPosts(): List<Post> {
    delay(1000)
    return listOf(Post("Title"))
}

fun main() = runBlocking {
    val user = async { fetchUser() }
    val posts = async { fetchPosts() }

    println("User: ${user.await()}")
    println("Posts: ${posts.await()}")
}

// Suspend functions
suspend fun doWork() {
    delay(1000)
    println("Work done")
}
💡 suspend functions can call other suspend functions
⚡ async for concurrent execution, await for results
📌 Structured concurrency ensures child coroutines complete
🟢 Use runBlocking only in main or tests

Coroutine Context

Dispatchers and coroutine context

javascript
// Dispatchers
fun main() = runBlocking {
    launch(Dispatchers.Main) {
        // UI updates (Android)
    }

    launch(Dispatchers.IO) {
        // IO operations
        val data = readFile()
    }

    launch(Dispatchers.Default) {
        // CPU-intensive work
        val result = complexCalculation()
    }

    launch(Dispatchers.Unconfined) {
        // Not confined to any thread
    }
}

// withContext - switching context
suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    // Perform IO operation
    readFromNetwork()
}

suspend fun processData() = withContext(Dispatchers.Default) {
    // CPU-intensive processing
    parseData()
}

// Flow - cold asynchronous stream
fun numbersFlow(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}

fun main() = runBlocking {
    numbersFlow()
        .map { it * it }
        .filter { it % 2 == 0 }
        .collect { println(it) }
}
💡 Dispatchers determine which thread coroutines run on
⚡ Flow is cold (starts on collect), Channel is hot
📌 withContext switches context without creating new coroutine
🟢 StateFlow holds state, SharedFlow broadcasts events

Generics

Generic types and variance

Generic Classes & Functions

Type parameters and constraints

javascript
// Generic class
class Box<T>(val value: T) {
    fun get(): T = value
}

val intBox = Box(42)
val stringBox = Box("Hello")

// Generic function
fun <T> singletonList(item: T): List<T> {
    return listOf(item)
}

val list = singletonList("item")

// Multiple type parameters
class Pair<A, B>(val first: A, val second: B) {
    fun swap(): Pair<B, A> = Pair(second, first)
}

// Type constraints
fun <T : Comparable<T>> max(a: T, b: T): T {
    return if (a > b) a else b
}

// Multiple constraints with where
fun <T> process(value: T)
    where T : CharSequence,
          T : Comparable<T> {
    println("Length: ${value.length}")
    println("Value: $value")
}
💡 out for covariance (producers), in for contravariance (consumers)
⚡ reified allows using type parameter in is checks
📌 Star projection (*) represents unknown type argument
🟢 Use where clause for multiple type constraints

Delegation

Class and property delegation

Delegation Patterns

Class delegation and delegated properties

javascript
// Class delegation
interface Base {
    fun print()
    val message: String
}

class BaseImpl(val x: Int) : Base {
    override fun print() = println(x)
    override val message = "BaseImpl: $x"
}

class Derived(b: Base) : Base by b {
    // Can override members
    override val message = "Derived message"
}

val base = BaseImpl(10)
val derived = Derived(base)
derived.print()  // Delegates to base

// Delegated properties
class User {
    // Lazy property
    val lazyValue: String by lazy {
        println("Computing lazy value")
        "Hello"
    }

    // Observable property
    var name: String by Delegates.observable("Initial") {
        prop, old, new ->
        println("$old -> $new")
    }

    // Vetoable property
    var age: Int by Delegates.vetoable(0) {
        prop, old, new ->
        new >= 0  // Reject negative values
    }
}
💡 by keyword enables delegation for classes and properties
⚡ lazy delegates compute value only once on first access
📌 observable and vetoable track/validate property changes
🟢 Map delegation useful for dynamic property storage

DSL Building

Creating domain-specific languages

DSL Construction

Building type-safe DSLs

javascript
// HTML DSL example
class HTML {
    private val children = mutableListOf<Element>()

    fun body(init: Body.() -> Unit) {
        val body = Body()
        body.init()
        children.add(body)
    }
}

class Body : Element() {
    fun h1(text: String, init: H1.() -> Unit = {}) {
        val h1 = H1(text)
        h1.init()
        children.add(h1)
    }

    fun p(text: String) {
        children.add(P(text))
    }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

// Usage
val doc = html {
    body {
        h1("Title") {
            // H1 configuration
        }
        p("Paragraph text")
    }
}
💡 Lambda with receiver enables DSL syntax
⚡ @DslMarker prevents scope leaking in nested DSLs
📌 Infix functions create natural language-like APIs
🟢 Type-safe builders provide compile-time validation

Annotations & Reflection

Custom annotations and reflection

Annotations

Creating and using annotations

javascript
// Annotation declaration
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class MyAnnotation(
    val name: String,
    val version: Int = 1
)

// Using annotations
@MyAnnotation(name = "Example", version = 2)
class AnnotatedClass {
    @Deprecated("Use newMethod instead")
    fun oldMethod() {}

    @JvmStatic
    fun staticMethod() {}

    @Throws(IOException::class)
    fun riskyMethod() {
        throw IOException("Error")
    }
}

// Built-in annotations
class Example {
    @JvmField
    val field = "Public field in Java"

    @JvmOverloads
    fun method(a: String = "default") {}

    @Suppress("UNCHECKED_CAST")
    fun uncheckedCast(obj: Any): List<String> {
        return obj as List<String>
    }
}
💡 @Target specifies where annotation can be used
⚡ @Retention determines if annotation is available at runtime
📌 Reflection requires kotlin-reflect dependency
🟢 Use @Jvm* annotations for Java interoperability

Interoperability

Java interop and platform types

Java Interop

Calling Java from Kotlin and vice versa

javascript
// Calling Java from Kotlin
val list = ArrayList<String>()  // Java class
list.add("item")
list.remove("item")

// Platform types
val item = list[0]  // Platform type String!
// Can be treated as nullable or non-null
val nullable: String? = item
val nonNull: String = item  // May throw NPE

// Java getters/setters as properties
// Java: class Person { getName(); setName(String) }
val person = Person()
person.name = "Alice"  // Calls setName
val name = person.name  // Calls getName

// Static members
val result = Math.max(5, 10)  // Java static method
val pi = Math.PI  // Java static field

// SAM conversion
val runnable = Runnable {
    println("Running")
}

button.setOnClickListener { view ->
    // SAM conversion for Java interfaces
}
💡 Platform types (!) can be null, handle carefully
⚡ @JvmStatic makes companion members static in Java
📌 SAM conversion works for Java functional interfaces
🟢 Use @JvmOverloads for Java-friendly default parameters

Advanced Features

Inline classes, contracts, and more

Inline & Value Classes

Performance optimizations

javascript
// Inline functions
inline fun measureTimeMillis(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}

// No function call overhead
val time = measureTimeMillis {
    // Code is inlined here
    Thread.sleep(100)
}

// Inline classes (value classes)
@JvmInline
value class UserId(val id: Long) {
    init {
        require(id > 0) { "Id must be positive" }
    }

    val displayId: String
        get() = "USER_$id"
}

// No boxing overhead at runtime
fun getUser(userId: UserId) {
    // userId is passed as primitive long
}

// Crossinline and noinline
inline fun inlineFunc(
    crossinline body: () -> Unit,  // Can't use return
    noinline callback: () -> Unit   // Not inlined
) {
    val runnable = object : Runnable {
        override fun run() {
            body()  // OK with crossinline
        }
    }
    callback()  // Regular function call
}
💡 inline eliminates function call overhead
⚡ Value classes wrap primitives without runtime overhead
📌 Contracts tell compiler about function behavior
🟢 Type aliases improve code readability