Jump to section
Kotlin Cheatsheet
A practical Kotlin guide covering syntax, null safety, OOP, coroutines, collections, extensions, and Android development workflows.
What is Kotlin?
Kotlin is a modern programming language that runs on the Java Virtual Machine. It's the official language for Android development — and honestly, it's a lot cleaner to write than Java.
where kotlin is used
- Google's officially recommended language for Android apps.
- Replaced Java as the standard — most new Android code is Kotlin.
- Runs on the JVM — works with Spring Boot and Ktor.
- Fully interoperable with Java libraries.
- Share code between Android, iOS, and web.
- Write business logic once — run it everywhere.
your first kotlin program
Kotlin
// every Kotlin program starts here
fun main() {
println("Hello, Kotlin!")
}
// println prints with a new line
// print prints without a new line
fun main() {
print("Hello, ")
print("World!")
// output: Hello, World!
}Variables & Data Types
Kotlin has two kinds of variables: ones you can change and ones you can't. The rule of thumb is simple — start with val, switch to var only when you actually need to change the value.
val vs var
Kotlin
// val: cannot be reassigned
val name = "Alice"
// name = "Bob" ← error
// var: can be reassigned
var score = 0
score = 10 // fine
score += 5 // also fine
// Kotlin infers the type automatically
val city = "Colombo" // String
val age = 25 // Int
val pi = 3.14 // Double
val done = true // Booleanbasic data types
Kotlin
val count: Int = 42
val big: Long = 9_000_000_000L
val price: Double = 19.99
val ratio: Float = 0.5F
val active: Boolean = true
val name: String = "Alice"
val grade: Char = 'A'
// underscores make big numbers readable
val million = 1_000_000type conversion
Kotlin
val num: Int = 42
// convert to other types
val asLong: Long = num.toLong()
val asDouble: Double = num.toDouble()
val asString: String = num.toString()
// convert from String
val parsed: Int = "99".toInt()
val safe: Int? = "abc".toIntOrNull()
// toIntOrNull returns null if it fails
// instead of throwing an exception
// ❌ this does not compile in Kotlin
// val x: Long = 42 ← type mismatchStrings
String templates are one of the first things Kotlin developers love. Instead of joining strings with +, you embed variables directly inside the text with a $ sign.
string templates
Kotlin
val name = "Alice"
val score = 95
// simple variable
println("Hello, $name")
// output: Hello, Alice
// expression inside ${}
println("Score: ${score * 2}")
// output: Score: 190
// property access
println("Length: ${name.length}")
// output: Length: 5
// ❌ old Java style (avoid)
println("Hello, " + name + "!")multiline strings
Kotlin
// triple quotes: no escape needed
val message = """
Hello, Alice!
Welcome to Kotlin.
Enjoy your stay.
""".trimIndent()
// trimIndent removes leading spaces
// raw strings keep everything as-is
val path = """C:UsersAliceDocuments"""
// no need to escape backslashescommon string functions
Kotlin
val text = " Hello, Kotlin! "
text.length // 18
text.trim() // "Hello, Kotlin!"
text.uppercase() // " HELLO, KOTLIN! "
text.lowercase() // " hello, kotlin! "
val clean = text.trim()
clean.contains("Kotlin") // true
clean.startsWith("Hello") // true
clean.endsWith("!") // true
clean.replace("Kotlin", "World")
// "Hello, World!"
clean.split(", ")
// ["Hello", "Kotlin!"]
clean.substring(7, 13)
// "Kotlin"
"".isEmpty() // true
" ".isBlank() // true (blank = only spaces)escape characters
Kotlin
// common escape sequences
println("Line one
Line two")
// Line one
// Line two
println("Name: Alice")
// Name: Alice
println("She said "hello"")
// She said "hello"
println("Path: C:\Users\Alice")
// Path: C:UsersAlice
// tip: use triple quotes to avoid escaping
val path = """C:UsersAlice"""Control Flow
Kotlin's control flow looks familiar but has a few tricks. if and when can return values — so instead of writing a separate variable assignment, you can write everything in one expression.
if — also an expression
Kotlin
// standard if
val age = 20
if (age >= 18) {
println("Adult")
} else {
println("Minor")
}
// if as an expression — returns a value
val label = if (age >= 18) "Adult" else "Minor"
// else-if chain
val score = 75
val grade = if (score >= 90) {
"A"
} else if (score >= 75) {
"B"
} else if (score >= 60) {
"C"
} else {
"F"
}when — smarter switch
Kotlin
val grade = "B"
// basic when
when (grade) {
"A" -> println("Excellent!")
"B" -> println("Good job!")
"C" -> println("You passed.")
else -> println("Try again.")
}
// when as expression
val feedback = when (grade) {
"A" -> "Excellent!"
"B", "C" -> "Good enough" // multiple values
else -> "Try again"
}
// when with ranges
val score = 82
val result = when (score) {
in 90..100 -> "A"
in 75..89 -> "B"
in 60..74 -> "C"
else -> "F"
}
// when without argument (replaces if-else chain)
val x = 15
when {
x < 0 -> println("Negative")
x == 0 -> println("Zero")
x > 0 -> println("Positive")
}for loops and ranges
Kotlin
// inclusive range: 1 to 5
for (i in 1..5) {
print("$i ") // 1 2 3 4 5
}
// exclusive end: 1 to 4 (not 5)
for (i in 1 until 5) {
print("$i ") // 1 2 3 4
}
// count down
for (i in 5 downTo 1) {
print("$i ") // 5 4 3 2 1
}
// step: every 2nd number
for (i in 0..10 step 2) {
print("$i ") // 0 2 4 6 8 10
}
// iterate a list
val names = listOf("Alice", "Bob", "Carol")
for (name in names) {
println(name)
}
// with index
for ((index, name) in names.withIndex()) {
println("$index: $name")
}while and do-while
Kotlin
var count = 0
// while: checks condition first
while (count < 5) {
println(count)
count++
}
// do-while: runs at least once
do {
println("count is $count")
count++
} while (count < 3)
// break: exit the loop early
for (i in 1..10) {
if (i == 5) break
print("$i ") // 1 2 3 4
}
// continue: skip this iteration
for (i in 1..5) {
if (i == 3) continue
print("$i ") // 1 2 4 5
}Functions
Functions in Kotlin start with fun. That's not just a keyword — writing Kotlin functions really is more enjoyable than Java. Default parameters alone eliminate a huge amount of boilerplate.
basic functions
Kotlin
// no return value (Unit)
fun greet() {
println("Hello!")
}
// with parameters
fun greet(name: String) {
println("Hello, $name!")
}
// with return type
fun add(a: Int, b: Int): Int {
return a + b
}
// call the functions
greet() // Hello!
greet("Alice") // Hello, Alice!
val sum = add(3, 4) // 7single-expression functions
Kotlin
// standard form
fun square(n: Int): Int {
return n * n
}
// single-expression form — same thing
fun square(n: Int) = n * n
// works with any expression
fun greet(name: String) =
"Hello, $name!"
fun max(a: Int, b: Int) =
if (a > b) a else bdefault and named parameters
Kotlin
// default parameters
fun createUser(
name: String,
role: String = "user",
active: Boolean = true
) {
println("$name | $role | $active")
}
createUser("Alice")
// Alice | user | true
createUser("Bob", "admin")
// Bob | admin | true
// named parameters — skip to the one you need
createUser(name = "Carol", active = false)
// Carol | user | false
// order doesn't matter with named params
createUser(
active = true,
role = "editor",
name = "Dave"
)vararg — variable number of arguments
Kotlin
// vararg accepts any number of arguments
fun printAll(vararg items: String) {
for (item in items) println(item)
}
printAll("Apple", "Banana", "Cherry")
// Apple
// Banana
// Cherry
// spread operator: pass an array as vararg
val fruits = arrayOf("Mango", "Kiwi")
printAll(*fruits)
// mix with regular params
fun log(tag: String, vararg messages: String) {
for (msg in messages) {
println("[$tag] $msg")
}
}
log("INFO", "Started", "Connected", "Ready")Null Safety
Null safety is one of the biggest reasons developers switch to Kotlin. The language forces you to think about null at compile time — so you get an error before the app crashes, not after.
nullable types with ?
Kotlin
// non-nullable: cannot hold null
val name: String = "Alice"
// name = null ← compile error
// nullable: can hold null
val nickname: String? = null
val city: String? = "Colombo"
// Kotlin forces you to handle the null
// before you can use a nullable value
println(nickname.length)
// ← compile error: nickname might be nullsafe call ?. and Elvis operator ?:
Kotlin
val name: String? = null
val city: String? = "Colombo"
// safe call: returns null if name is null
// instead of throwing an exception
println(name?.length) // null
println(city?.length) // 7
// Elvis operator: fallback when null
val len = name?.length ?: 0
println(len) // 0
// chain safe calls
data class Address(val city: String?)
data class User(val address: Address?)
val user: User? = User(Address("Colombo"))
val cityName = user?.address?.city ?: "Unknown"
println(cityName) // Colombonon-null assertion !! and smart cast
Kotlin
val name: String? = "Alice"
// !! forces non-null — crashes if null
val length = name!!.length // 5
// smart cast: Kotlin knows it's non-null
// after an if-null check
if (name != null) {
// inside here, name is treated as String
println(name.length) // no ?. needed
}
// let: run a block only when non-null
name?.let {
// 'it' is the non-null value
println("Name is: $it")
println("Length: ${it.length}")
}Every !! in your code is a potential crash. If you find yourself using it often, the design probably needs rethinking.
Collections
Kotlin splits collections into two groups: read-only and mutable. This is intentional — it forces you to be clear about whether a list is meant to change or not.
list — ordered collection
Kotlin
// read-only list
val fruits = listOf("Apple", "Banana", "Mango")
fruits[0] // "Apple"
fruits.first() // "Apple"
fruits.last() // "Mango"
fruits.size // 3
fruits.contains("Banana") // true
// mutable list
val scores = mutableListOf(10, 20, 30)
scores.add(40) // [10, 20, 30, 40]
scores.remove(20) // [10, 30, 40]
scores[0] = 99 // [99, 30, 40]
scores.removeAt(1) // [99, 40]
// convert between the two
val readOnly = scores.toList()
val mutable = fruits.toMutableList()set and map
Kotlin
// set: no duplicate values
val tags = setOf("kotlin", "android", "kotlin")
println(tags) // [kotlin, android]
tags.contains("android") // true
val mutableTags = mutableSetOf("a", "b")
mutableTags.add("c")
mutableTags.remove("a")
// map: key-value pairs
val capitals = mapOf(
"Sri Lanka" to "Colombo",
"Japan" to "Tokyo",
"France" to "Paris"
)
capitals["Japan"] // "Tokyo"
capitals.keys // [Sri Lanka, Japan, France]
capitals.values // [Colombo, Tokyo, Paris]
capitals.containsKey("France") // true
// mutable map
val scores = mutableMapOf("Alice" to 90)
scores["Bob"] = 85 // add entry
scores["Alice"] = 95 // update entry
scores.remove("Bob") // remove entryiterating collections
Kotlin
val names = listOf("Alice", "Bob", "Carol")
// for loop
for (name in names) {
println(name)
}
// forEach with lambda
names.forEach { name ->
println(name)
}
// forEach with implicit 'it'
names.forEach { println(it) }
// with index
names.forEachIndexed { index, name ->
println("$index: $name")
}
// iterating a map
val scores = mapOf("Alice" to 90, "Bob" to 85)
for ((name, score) in scores) {
println("$name scored $score")
}filter, map, find and more
Kotlin
val nums = listOf(1, 2, 3, 4, 5, 6)
// keep only even numbers
nums.filter { it % 2 == 0 }
// [2, 4, 6]
// multiply each by 10
nums.map { it * 10 }
// [10, 20, 30, 40, 50, 60]
// find first even number (or null)
nums.find { it % 2 == 0 } // 2
// check conditions
nums.any { it > 5 } // true
nums.all { it > 0 } // true
nums.none { it > 10 } // true
nums.count { it % 2 == 0 } // 3
// sort by value
val names = listOf("Carol", "Alice", "Bob")
names.sortedBy { it }
// [Alice, Bob, Carol]
// group into a map
nums.groupBy { if (it % 2 == 0) "even" else "odd" }
// {odd=[1,3,5], even=[2,4,6]}Classes & Objects
Kotlin classes are much less verbose than Java. A data class with three properties takes one line. You get equals, hashCode, toString, and copy for free.
primary constructor and properties
Kotlin
// class with properties in the constructor
class Person(
val name: String,
var age: Int
) {
// init block runs on creation
init {
println("Created: $name")
}
// member function
fun greet() = "Hi, I'm $name"
}
// create an instance (no 'new' keyword)
val alice = Person("Alice", 28)
println(alice.name) // Alice
println(alice.age) // 28
alice.age = 29 // var can change
println(alice.greet()) // Hi, I'm Alicedata class — model classes made easy
Kotlin
data class User(
val id: Int,
val name: String,
val email: String
)
val user = User(1, "Alice", "[email protected]")
// toString: readable output
println(user)
// User(id=1, name=Alice, email=alice@...)
// equals: compares values
val user2 = User(1, "Alice", "[email protected]")
println(user == user2) // true
// copy: change only what you need
val updated = user.copy(email = "[email protected]")
println(updated)
// User(id=1, name=Alice, email=new@...)
// destructuring
val (id, name, email) = user
println("$id $name $email")object — singleton
Kotlin
// object: single instance, created lazily
object AppConfig {
val apiUrl = "https://api.example.com"
val timeout = 30
fun baseHeaders() = mapOf(
"Content-Type" to "application/json"
)
}
AppConfig.apiUrl // access directly
AppConfig.baseHeaders()
// companion object: class-level members
class User(val name: String) {
companion object {
const val MAX_NAME_LENGTH = 50
// factory function
fun guest() = User("Guest")
}
}
val guest = User.guest()
println(User.MAX_NAME_LENGTH) // 50secondary constructor and properties
Kotlin
// secondary constructor
class Person(val name: String, val age: Int) {
constructor(name: String) : this(name, 0)
}
val baby = Person("Max") // age = 0
val adult = Person("Alice", 28)
// computed property with custom getter
class Circle(val radius: Double) {
val area: Double
get() = Math.PI * radius * radius
val diameter: Double
get() = radius * 2
}
val c = Circle(5.0)
println(c.area) // 78.539...
println(c.diameter) // 10.0OOP — Inheritance & Interfaces
Kotlin classes are final by default — you have to explicitly mark them open to allow inheritance. That's worth noting, because it's the opposite of Java.
inheritance — open and override
Kotlin
// base class must be 'open'
open class Animal(val name: String) {
open fun speak() {
println("$name makes a sound")
}
}
// subclass
class Dog(name: String) : Animal(name) {
override fun speak() {
println("$name barks")
}
}
class Cat(name: String) : Animal(name) {
override fun speak() {
println("$name meows")
}
}
val dog = Dog("Rex")
dog.speak() // Rex barks
// call parent function
class Puppy(name: String) : Dog(name) {
override fun speak() {
super.speak() // Rex barks
println("...softly")
}
}abstract class and interface
Kotlin
// abstract class
abstract class Shape {
abstract fun area(): Double
// concrete function
fun describe() = "I am a ${javaClass.simpleName}"
}
class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
}
// interface
interface Clickable {
fun onClick()
// default implementation
fun onLongClick() {
println("Long clicked!")
}
}
interface Focusable {
fun onFocus()
}
// implement multiple interfaces
class Button : Clickable, Focusable {
override fun onClick() {
println("Button clicked")
}
override fun onFocus() {
println("Button focused")
}
}sealed class — restricted hierarchy
Kotlin
// sealed class: all subclasses known at compile time
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
// when is exhaustive — no else needed
fun handle(result: Result) {
when (result) {
is Result.Success ->
println("Data: ${result.data}")
is Result.Error ->
println("Error: ${result.message}")
is Result.Loading ->
println("Loading...")
}
}
val r: Result = Result.Success("Hello")
handle(r) // Data: Hellovisibility modifiers
Kotlin
class Account(
val owner: String,
private var balance: Double
) {
// public by default
fun getBalance() = balance
// only visible inside this class
private fun validate(amount: Double) =
amount > 0 && amount <= balance
// visible in this class and subclasses
protected fun log(msg: String) {
println("[Account] $msg")
}
fun withdraw(amount: Double) {
if (validate(amount)) {
balance -= amount
log("Withdrew $amount")
}
}
}Lambdas & Higher-Order Functions
A lambda is a function without a name. You pass it directly as an argument. Higher-order functions accept or return other functions — and they show up constantly in Kotlin code.
lambda syntax
Kotlin
// lambda stored in a variable
val double = { x: Int -> x * 2 }
println(double(5)) // 10
// single parameter: use 'it'
val greet = { name: String -> "Hello, $name!" }
println(greet("Alice")) // Hello, Alice!
// passing lambda to a function
val nums = listOf(1, 2, 3, 4, 5)
// full syntax
nums.filter({ it % 2 == 0 })
// trailing lambda: outside parens
nums.filter { it % 2 == 0 }
// multiple parameters
val add = { a: Int, b: Int -> a + b }
println(add(3, 4)) // 7higher-order functions
Kotlin
// function that accepts another function
fun operate(
a: Int,
b: Int,
operation: (Int, Int) -> Int
): Int {
return operation(a, b)
}
val sum = operate(3, 4) { x, y -> x + y }
val product = operate(3, 4) { x, y -> x * y }
println(sum) // 7
println(product) // 12
// function that returns a function
fun multiplier(factor: Int): (Int) -> Int {
return { num -> num * factor }
}
val triple = multiplier(3)
println(triple(5)) // 15
println(triple(10)) // 30scope functions — let, run, apply, also, with
Kotlin
data class User(
var name: String = "",
var email: String = ""
)
// let: non-null block, 'it' = the value
val name: String? = "Alice"
name?.let {
println("Name: $it")
println("Length: ${it.length}")
}
// apply: configure an object, returns it
val user = User().apply {
name = "Alice"
email = "[email protected]"
}
// also: side effect, returns original object
val nums = mutableListOf(1, 2, 3)
.also { println("Created: $it") }
nums.add(4)
// run: block + return result
val length = name?.run {
println("Running on: $this")
this.length // returned
}
// with: call methods on an object
val result = with(user) {
"$name <$email>"
}Extension Functions
Extension functions let you add new functions to any existing class — without touching the original code. That said, they're not magic. They don't actually modify the class.
extension functions
Kotlin
// add a function to String
fun String.shout(): String {
return this.uppercase() + "!!!"
}
println("hello".shout()) // HELLO!!!
// add a function to Int
fun Int.isEven(): Boolean = this % 2 == 0
println(4.isEven()) // true
println(7.isEven()) // false
// extend a custom class
data class User(val name: String, val age: Int)
fun User.isAdult() = age >= 18
val alice = User("Alice", 25)
println(alice.isAdult()) // true
// extend a nullable type
fun String?.orDefault(default: String): String {
return this ?: default
}
val name: String? = null
println(name.orDefault("Guest")) // Guestextension properties
Kotlin
// extension property — no backing field allowed
val String.wordCount: Int
get() = this.trim().split(" ").size
val String.isPalindrome: Boolean
get() = this == this.reversed()
println("hello world".wordCount) // 2
println("racecar".isPalindrome) // true
println("kotlin".isPalindrome) // false
// on a list
val <T> List<T>.secondOrNull: T?
get() = if (size >= 2) this[1] else null
val nums = listOf(10, 20, 30)
println(nums.secondOrNull) // 20Coroutines Basics
Coroutines are Kotlin's way to handle async work — network calls, database reads, anything that takes time. The code looks synchronous, but it doesn't block the thread. That's the whole idea.
suspend functions
Kotlin
import kotlinx.coroutines.*
// suspend: can pause without blocking
suspend fun fetchUser(id: Int): String {
delay(1000) // pause for 1 second
return "User $id"
}
// must be called from a coroutine
suspend fun loadData() {
val user = fetchUser(1) // waits here
println("Loaded: $user") // then continues
}launch and runBlocking
Kotlin
import kotlinx.coroutines.*
// runBlocking: for main() and tests
fun main() = runBlocking {
println("Start")
// launch: runs concurrently
val job = launch {
delay(1000)
println("Coroutine done")
}
println("After launch")
job.join() // wait for it to finish
println("End")
}
// output:
// Start
// After launch
// Coroutine done
// EndAvoid GlobalScope in production code. In Android, use viewModelScope (ViewModel) or lifecycleScope (Activity/Fragment) so coroutines cancel automatically.
async and await — get a result back
Kotlin
import kotlinx.coroutines.*
suspend fun loadName(): String {
delay(500)
return "Alice"
}
suspend fun loadAge(): Int {
delay(500)
return 28
}
fun main() = runBlocking {
// run both at the same time
val nameDeferred = async { loadName() }
val ageDeferred = async { loadAge() }
// await: pause until each result is ready
val name = nameDeferred.await()
val age = ageDeferred.await()
// both finished in ~500ms, not ~1000ms
println("$name is $age")
}Tips & Good Habits
These habits make Kotlin code cleaner and safer. Most of them come down to the same principle: use the language features that are there for a reason.
prefer val over var
Kotlin
// ❌ var everywhere — hard to track changes
var userId = 1
var userName = "Alice"
var isAdmin = false
// ✅ val where possible
val userId = 1 // will never change
val userName = "Alice"
// var only when needed
var retryCount = 0
retryCount++avoid !! — use safe alternatives
Kotlin
val name: String? = getName()
// ❌ crashes if name is null
println(name!!.length)
// ✅ safe call + Elvis
println(name?.length ?: 0)
// ✅ let: only runs if non-null
name?.let { println(it.length) }
// ✅ early return
fun process(name: String?) {
name ?: return // exit if null
// name is non-null from here
println(name.length)
}
// ✅ requireNotNull: throws with a useful message
fun process(name: String?) {
val safe = requireNotNull(name) {
"Name must not be null"
}
println(safe.length)
}Every !! is a crash waiting to happen. If you're writing !! often, the problem is usually that the type should be non-null from the start.
use data class for models
Kotlin
// ❌ regular class: no equals, no toString
class User(val name: String, val age: Int)
val a = User("Alice", 28)
val b = User("Alice", 28)
println(a == b) // false (different objects)
println(a) // User@3a4b5c6d (useless)
// ✅ data class: everything for free
data class User(val name: String, val age: Int)
val a = User("Alice", 28)
val b = User("Alice", 28)
println(a == b) // true
println(a) // User(name=Alice, age=28)
// copy is especially useful
val updated = a.copy(age = 29)
println(updated) // User(name=Alice, age=29)
println(a) // User(name=Alice, age=28)use when over long if-else chains
Kotlin
// ❌ long if-else chain — hard to read
fun describe(code: Int): String {
return if (code == 200) {
"OK"
} else if (code == 201) {
"Created"
} else if (code == 400) {
"Bad Request"
} else if (code == 404) {
"Not Found"
} else {
"Unknown"
}
}
// ✅ when expression — clean and readable
fun describe(code: Int) = when (code) {
200 -> "OK"
201 -> "Created"
400 -> "Bad Request"
404 -> "Not Found"
in 500..599 -> "Server Error"
else -> "Unknown"
}handy one-liners worth bookmarking
Kotlin
// repeat a block N times
repeat(3) { println("Hello") }
// takeIf: return value if condition is true
val name = "Alice"
val result = name.takeIf { it.length > 3 }
// "Alice" — condition passed
// takeUnless: return value if condition is false
val short = name.takeUnless { it.length > 3 }
// null — condition was true, so return null
// joinToString: list to formatted string
val items = listOf("A", "B", "C")
items.joinToString(", ") // "A, B, C"
items.joinToString(" | ") // "A | B | C"
// getOrElse: safe list access with fallback
val nums = listOf(10, 20, 30)
nums.getOrElse(5) { 0 } // 0 (index 5 missing)
// also great for debug logging
val user = User("Alice", 28)
.also { println("Created: $it") }No login required to share feedback
More Cheatsheets
Keep your reference handy
Explore more zero-to-hero cheatsheets for the tools you use daily.