Toolmingo
Guides14 min read

Kotlin Cheat Sheet: Syntax, Coroutines, OOP & Android

A complete Kotlin cheat sheet — variables, null safety, coroutines, OOP, collections, extension functions, and Android patterns with copy-ready examples for Kotlin 2.x.

Kotlin is a statically typed, multiplatform language that compiles to JVM bytecode, JavaScript, and native binaries. It is the official language for Android development. This cheat sheet covers Kotlin 2.x — from null safety and data classes to coroutines, Flow, and Android patterns — with copy-ready examples.

Quick reference

The 25 patterns that cover 95% of everyday Kotlin development.

Pattern Example
val (immutable) val name = "Alice"
var (mutable) var count = 0
Nullable var s: String? = null
Safe call s?.length
Elvis operator s?.length ?: 0
Non-null assert s!!.length
String template "Hello, $name! Length ${name.length}"
Data class data class User(val id: Int, val name: String)
When expression when (x) { 1 -> "one"; else -> "other" }
Range for (i in 1..10) { }
List val list = listOf("a", "b", "c")
Map val map = mapOf("key" to "value")
Lambda val double = { x: Int -> x * 2 }
Extension function fun String.shout() = uppercase() + "!"
Scope function let value?.let { println(it) }
Scope function also list.also { println("Size: ${it.size}") }
Scope function apply TextView(ctx).apply { text = "Hi" }
Scope function run val result = sb.run { append("!"); toString() }
Object declaration object Singleton { val x = 42 }
Companion object companion object { fun create() = MyClass() }
Sealed class sealed class Result<out T>
Coroutine launch viewModelScope.launch { fetchData() }
Flow collect flow.collect { value -> process(value) }
Destructuring val (id, name) = user
Higher-order fun fun transform(x: Int, fn: (Int) -> Int) = fn(x)

Variables and types

// Immutable (preferred)
val name: String = "Alice"   // explicit type
val age = 30                  // inferred type

// Mutable
var count = 0
count++

// Constants (compile-time)
const val MAX_SIZE = 100

// Type aliases
typealias UserId = Int
typealias UserMap = Map<UserId, String>

// Basic types
val b: Byte   = 127
val s: Short  = 32_000
val i: Int    = 1_000_000
val l: Long   = 9_000_000_000L
val f: Float  = 3.14f
val d: Double = 3.14159265358979
val c: Char   = 'A'
val flag: Boolean = true

// Type conversions (explicit, no implicit widening)
val x: Int = 42
val y: Long = x.toLong()
val z: Double = x.toDouble()
val s2: String = x.toString()
val n: Int = "42".toInt()
val n2: Int? = "abc".toIntOrNull()   // null-safe parse

Null safety

Kotlin eliminates NullPointerException at compile time.

// Nullable types — append ?
var name: String? = null
var length: Int? = name?.length        // safe call — null if name is null

// Elvis operator — default when null
val len = name?.length ?: 0           // 0 if name is null

// Non-null assertion — throws NPE if null (avoid)
val forced = name!!.length

// Smart cast after null check
if (name != null) {
    println(name.length)              // auto-cast to String
}

// let — run block only when non-null
name?.let { n ->
    println("Name is $n, length ${n.length}")
}

// requireNotNull / checkNotNull
val req: String = requireNotNull(name) { "name must not be null" }

// Safe cast — null if incompatible type
val obj: Any = "Hello"
val str: String? = obj as? String     // null instead of ClassCastException

// Nullable collections
val list: List<String?> = listOf("a", null, "c")
val nonNull: List<String> = list.filterNotNull()

Strings

val s = "Hello, World!"

// Templates
val name = "Kotlin"
println("Hello, $name!")                      // Hello, Kotlin!
println("1 + 1 = ${1 + 1}")                  // 1 + 1 = 2

// Multi-line (trimIndent preserves relative indentation)
val json = """
    {
        "key": "value"
    }
""".trimIndent()

// Common methods
s.length                      // 13
s.uppercase() / s.lowercase()
s.trim() / s.trimStart() / s.trimEnd()
s.split(", ")                 // List<String>
s.replace("World", "Kotlin")
s.startsWith("Hello")         // true
s.endsWith("!")               // true
s.contains("World")           // true
s.substring(0, 5)             // "Hello"
s.padStart(20, '*')
s.padEnd(20, '-')
s.repeat(3)
s.toInt() / s.toDouble() / s.toLongOrNull()

// String building
val sb = StringBuilder()
sb.append("Hello")
sb.append(", ")
sb.append("World")
val result = sb.toString()

// buildString (idiomatic Kotlin)
val text = buildString {
    append("Hello")
    append(", ")
    append("World")
}

Control flow

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

// when as expression (replaces switch)
val grade = when (score) {
    in 90..100 -> "A"
    in 80..89  -> "B"
    in 70..79  -> "C"
    else       -> "F"
}

// when on type
fun describe(obj: Any): String = when (obj) {
    is Int    -> "Integer: $obj"
    is String -> "String of length ${obj.length}"
    is List<*> -> "List with ${obj.size} items"
    null      -> "null"
    else      -> "Unknown: ${obj::class.simpleName}"
}

// Ranges and progressions
for (i in 1..10) print(i)          // 1 2 3 … 10
for (i in 1 until 10) print(i)     // 1 2 3 … 9 (excludes 10)
for (i in 10 downTo 1 step 2) print(i)  // 10 8 6 4 2
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

// while / do-while
while (condition) { }
do { } while (condition)

// Labels for nested loops
outer@ for (i in 1..5) {
    for (j in 1..5) {
        if (j == 3) break@outer
    }
}

Functions

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

// Single-expression function
fun double(x: Int) = x * 2

// Default parameters
fun createUser(name: String, role: String = "user", active: Boolean = true) { }

// Named arguments
createUser(name = "Alice", active = false)

// Vararg
fun sum(vararg numbers: Int) = numbers.sum()
sum(1, 2, 3, 4, 5)

// Local functions
fun processData(data: List<Int>): List<Int> {
    fun validate(n: Int) = n > 0           // only visible inside processData
    return data.filter { validate(it) }
}

// Higher-order functions
fun transform(x: Int, fn: (Int) -> Int) = fn(x)
transform(5, ::double)                     // 10
transform(5) { it * 3 }                   // 15

// Extension functions
fun String.isPalindrome(): Boolean {
    val cleaned = lowercase().filter { it.isLetterOrDigit() }
    return cleaned == cleaned.reversed()
}
"Racecar".isPalindrome()                  // true

// Infix functions
infix fun Int.add(other: Int) = this + other
val result = 3 add 4                      // 7

// Operator overloading
data class Vector(val x: Int, val y: Int) {
    operator fun plus(other: Vector) = Vector(x + other.x, y + other.y)
}

OOP: classes, interfaces, inheritance

// Primary constructor
class Person(val name: String, var age: Int) {
    // Secondary constructor
    constructor(name: String) : this(name, 0)

    // Init block
    init {
        require(name.isNotBlank()) { "Name must not be blank" }
    }

    // Property with custom getter
    val isAdult: Boolean
        get() = age >= 18

    // Method
    fun greet() = "Hello, I'm $name"
}

// Data class — auto equals/hashCode/toString/copy/componentN
data class User(val id: Int, val name: String, val email: String)

val alice = User(1, "Alice", "alice@example.com")
val bob = alice.copy(id = 2, name = "Bob")   // copy with changes
val (id, name, email) = alice                // destructuring

// Sealed class — restricted hierarchy, exhaustive when
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String, val cause: Throwable? = null) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun handle(result: Result<User>) = when (result) {
    is Result.Success -> println(result.data)
    is Result.Error   -> println(result.message)
    Result.Loading    -> println("Loading…")
}

// Interface
interface Drawable {
    val color: String
    fun draw()
    fun describe() = "A $color shape"    // default implementation
}

// Abstract class
abstract class Shape(open val color: String) : Drawable {
    abstract fun area(): Double
    override fun draw() = println("Drawing $color shape")
}

class Circle(override val color: String, val radius: Double) : Shape(color) {
    override fun area() = Math.PI * radius * radius
}

// Object declaration (singleton)
object Config {
    const val BASE_URL = "https://api.example.com"
    val timeout = 30_000L
}

// Companion object (static-like members)
class MyClass private constructor(val value: Int) {
    companion object {
        fun create(value: Int) = MyClass(value)
        const val DEFAULT = 0
    }
}
val obj = MyClass.create(42)

// Enum class
enum class Direction(val degrees: Int) {
    NORTH(0), EAST(90), SOUTH(180), WEST(270);
    fun opposite() = values()[(ordinal + 2) % 4]
}

Collections

Kotlin collections are immutable by default; use mutable variants to mutate.

// List
val immutable = listOf(1, 2, 3)
val mutable   = mutableListOf(1, 2, 3)
mutable.add(4)
mutable.removeAt(0)
mutable[0] = 10

// Set
val set    = setOf("a", "b", "c")
val mSet   = mutableSetOf("a", "b")
mSet.add("c")

// Map
val map  = mapOf("one" to 1, "two" to 2)
val mMap = mutableMapOf("one" to 1)
mMap["three"] = 3
mMap.getOrDefault("four", 0)
mMap.getOrPut("five") { 5 }

// Array
val arr = arrayOf(1, 2, 3)
val int = IntArray(5) { it * 2 }   // [0, 2, 4, 6, 8]

// Common operations
list.filter { it > 2 }
list.map { it * 2 }
list.flatMap { listOf(it, it * 2) }
list.reduce { acc, n -> acc + n }
list.fold(0) { acc, n -> acc + n }
list.groupBy { it % 2 == 0 }      // Map<Boolean, List<Int>>
list.partition { it > 2 }         // Pair<List, List>
list.any { it > 3 }
list.all { it > 0 }
list.none { it < 0 }
list.count { it > 2 }
list.sumOf { it.toLong() }
list.maxOrNull() / list.minOrNull()
list.sorted() / list.sortedDescending()
list.sortedBy { it.length }        // for strings/objects
list.distinct()
list.zip(otherList)                // List<Pair>
list.chunked(3)                    // List<List<T>> in chunks of 3
list.windowed(3)                   // sliding window
list.take(3) / list.drop(3)
list.first() / list.last() / list.firstOrNull()
list.indexOf("a") / list.contains("a")
list.joinToString(", ")
list.toSet() / list.toMutableList()

Scope functions

Kotlin's scope functions — let, run, with, apply, also — execute a block on an object.

Function Receiver Returns Use case
let it lambda result null-check, transform
run this lambda result object config + compute
with this lambda result multiple calls on object
apply this receiver builder/initializer
also it receiver side-effects (logging)
// let — null-safe transform
val upper = name?.let { it.uppercase() + "!" }

// run — configure and compute result
val result = StringBuilder().run {
    append("Hello")
    append(", World")
    toString()   // returns this String
}

// with — multiple operations on same object (not extension)
val text = with(StringBuilder()) {
    append("Hello")
    append(", ")
    append("World")
    toString()
}

// apply — builder pattern, returns the receiver
val button = Button(context).apply {
    text = "Click me"
    setOnClickListener { /* … */ }
    isEnabled = true
}

// also — side effects, returns receiver unchanged
val list = mutableListOf(1, 2, 3)
    .also { println("Original: $it") }
    .also { it.add(4) }

Coroutines

Kotlin coroutines provide structured concurrency for async programming.

// Add dependencies
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

import kotlinx.coroutines.*

// Launch — fire and forget
fun main() = runBlocking {
    val job = launch {
        delay(1000)
        println("World!")
    }
    println("Hello,")
    job.join()
}

// async — returns Deferred<T>
suspend fun fetchData(): String = coroutineScope {
    val deferred1 = async { fetchUser() }
    val deferred2 = async { fetchPosts() }
    "${deferred1.await()} + ${deferred2.await()}"   // parallel
}

// suspend function
suspend fun fetchUser(): User {
    return withContext(Dispatchers.IO) {
        // blocking I/O goes here
        api.getUser()
    }
}

// Dispatchers
// Dispatchers.Main   — UI thread (Android)
// Dispatchers.IO     — I/O operations (network, disk)
// Dispatchers.Default — CPU-intensive work

// Coroutine scope — structured concurrency
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            try {
                val data = fetchData()
                _uiState.value = UiState.Success(data)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// withTimeout
val result = withTimeout(5000L) {
    fetchData()
}

// Cancellation — always cooperative
val job = launch {
    repeat(1000) { i ->
        if (!isActive) return@launch     // check cancellation
        delay(100)
        println("Working $i")
    }
}
job.cancel()

Flow

Flow is a cold, asynchronous stream — Kotlin's RxJava alternative.

import kotlinx.coroutines.flow.*

// Creating flows
val flow1 = flow {
    emit(1)
    delay(100)
    emit(2)
    emit(3)
}

val flow2 = flowOf(1, 2, 3, 4, 5)

val flow3 = (1..10).asFlow()

// Operators (lazy — nothing runs until collect)
flow3
    .filter { it % 2 == 0 }
    .map { it * it }
    .take(3)
    .collect { println(it) }   // 4, 16, 36

// transform
flow3.transform { value ->
    emit("Item: $value")
    emit("Double: ${value * 2}")
}

// combine two flows
combine(flow1, flow2) { a, b -> a + b }.collect { }

// zip two flows
flow1.zip(flow2) { a, b -> "$a-$b" }.collect { }

// flatMapConcat, flatMapMerge, flatMapLatest
flow3.flatMapLatest { id ->
    fetchUserFlow(id)          // cancels previous if new value arrives
}

// StateFlow — hot, always has a value (replaces LiveData)
class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}

// SharedFlow — hot, broadcast to multiple collectors
val sharedFlow = MutableSharedFlow<Event>(replay = 0)

// Collect in Compose
val uiState by viewModel.uiState.collectAsState()

// Error handling
flow1
    .catch { e -> emit(-1) }     // catch upstream errors
    .onEach { println(it) }
    .launchIn(viewModelScope)    // terminal operator

Generics

// Generic function
fun <T> singletonList(item: T): List<T> = listOf(item)

// Generic class with upper bound
class Box<T : Comparable<T>>(val value: T) {
    fun isGreaterThan(other: T) = value > other
}

// Multiple bounds
fun <T> clamp(value: T, min: T, max: T): T
    where T : Comparable<T>, T : Number = when {
    value < min -> min
    value > max -> max
    else -> value
}

// Variance
// out — covariant (producer), T only in out position
class Producer<out T>(val value: T)
val producer: Producer<Number> = Producer<Int>(42)    // OK

// in — contravariant (consumer), T only in in position
class Consumer<in T> {
    fun consume(value: T) { println(value) }
}
val consumer: Consumer<Int> = Consumer<Number>()      // OK

// Star projection — unknown type
fun printList(list: List<*>) = list.forEach { println(it) }

// reified type parameters (inline functions only)
inline fun <reified T> parseJson(json: String): T =
    gson.fromJson(json, T::class.java)
val user: User = parseJson(jsonString)

Android patterns

// ViewModel + StateFlow
data class UiState(
    val isLoading: Boolean = false,
    val users: List<User> = emptyList(),
    val error: String? = null
)

class UserViewModel(private val repo: UserRepository) : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState = _uiState.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val users = repo.getUsers()
                _uiState.update { it.copy(isLoading = false, users = users) }
            } catch (e: Exception) {
                _uiState.update { it.copy(isLoading = false, error = e.message) }
            }
        }
    }
}

// Jetpack Compose UI
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        uiState.isLoading -> CircularProgressIndicator()
        uiState.error != null -> Text("Error: ${uiState.error}")
        else -> LazyColumn {
            items(uiState.users) { user ->
                Text(user.name)
            }
        }
    }
}

// Room database
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: Int,
    val name: String,
    val email: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): Flow<List<UserEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: UserEntity)

    @Delete
    suspend fun delete(user: UserEntity)
}

// Retrofit API
interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<UserDto>

    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): UserDto

    @POST("users")
    suspend fun createUser(@Body user: UserDto): UserDto
}

Common mistakes

Mistake Problem Fix
s!!.length everywhere Crashes on null Use s?.length ?: 0
Using var by default Encourages mutation Default to val
mutableListOf in public API Exposes mutability Return List<T>, not MutableList<T>
object : Runnable { } in Kotlin Verbose SAM syntax Use lambda { }
Blocking on main thread ANR crashes Use Dispatchers.IO for I/O
Not using data class for DTOs Manual equals/copy Always use data class for value objects
Catching Exception silently Swallows bugs Log or re-throw
Ignoring coroutine cancellation Memory leaks Check isActive, use structured concurrency

Kotlin vs Java quick comparison

Feature Kotlin Java
Null safety Built-in (?, ?., ?:) Optional / annotations
Data classes data class record (Java 16+)
Extension functions Yes No
Coroutines Built-in CompletableFuture / Virtual threads
Default parameters Yes Overloads only
String templates "Hello, $name" String.format(...)
Sealed classes Yes (with when) sealed interface (Java 17+)
Smart casts Yes No
Checked exceptions No Yes
Operator overloading Yes No

FAQ

Q: Should I use Kotlin or Java for new Android projects?
Use Kotlin. Google recommends Kotlin-first for Android since 2019. All modern Jetpack libraries are designed for Kotlin and coroutines. New Android projects should start with Kotlin.

Q: What is the difference between val and const val?
val is a runtime-immutable property — its value is computed once but can depend on runtime state (e.g., a function call). const val is a compile-time constant and must be a primitive or String at the top level or inside an object/companion object.

Q: When should I use Flow vs LiveData?
Prefer Flow (specifically StateFlow/SharedFlow) for new code. LiveData is lifecycle-aware but Android-only. StateFlow is pure Kotlin, works across all Kotlin targets, and integrates better with Compose via collectAsStateWithLifecycle().

Q: What is the difference between launch and async?
launch starts a coroutine and returns a Job — use it for fire-and-forget operations. async returns a Deferred<T> — use it when you need a result (call await() to get it). Both run concurrently when called without await inside a coroutineScope.

Q: How do I convert a Kotlin data class to/from JSON?
Use Kotlin serialization: add @Serializable to the data class and call Json.encodeToString(obj) / Json.decodeFromString<User>(jsonString). Alternatively use Gson or Moshi. Kotlin serialization is preferred for multiplatform projects.

Q: What is the difference between object and companion object?
object declares a singleton class — a single instance accessible by its name. companion object is a special object scoped to a class, giving access to class-like static members (MyClass.create()). Both are initialized lazily on first access and are thread-safe.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools