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.