In the previous tutorial, you learned about functions. Now let’s learn about control flow — how to make decisions and repeat actions in your code.
Kotlin’s control flow is similar to Java, but with important improvements. if and when are expressions that return values. when replaces switch and is much more powerful.
In this tutorial, you will learn:
ifas an expressionwhen— Kotlin’s powerful pattern matching- Ranges
forloopswhileanddo-whileloopsbreak,continue, and labels
if Expression
In Java, if is a statement. In Kotlin, if is an expression — it returns a value.
// if as an expression — returns a value
val max = if (a > b) a else b
This means you don’t need the ternary operator (? :) from Java. Kotlin’s if does the same thing.
Basic if-else
fun maxOf(a: Int, b: Int): Int {
return if (a > b) a else b
}
println(maxOf(10, 20)) // 20
As a single-expression function:
fun minOf(a: Int, b: Int) = if (a < b) a else b
if-else if-else Chain
fun classifyTemperature(temp: Int): String {
return if (temp < 0) {
"Freezing"
} else if (temp < 15) {
"Cold"
} else if (temp < 25) {
"Nice"
} else if (temp < 35) {
"Hot"
} else {
"Very hot"
}
}
println(classifyTemperature(22)) // Nice
println(classifyTemperature(-5)) // Freezing
println(classifyTemperature(40)) // Very hot
When if is used as an expression (returning a value), the else branch is required. This makes sure the expression always has a value.
when Expression
when is Kotlin’s replacement for Java’s switch. It is much more powerful.
Basic when
fun dayType(day: String): String {
return when (day) {
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> "Weekday"
"Saturday", "Sunday" -> "Weekend"
else -> "Unknown"
}
}
println(dayType("Monday")) // Weekday
println(dayType("Saturday")) // Weekend
Each branch uses -> to separate the condition from the result. Multiple values can share a branch with commas.
when with Ranges
fun gradeDescription(score: Int): String {
return when (score) {
in 90..100 -> "Excellent"
in 80..89 -> "Good"
in 70..79 -> "Average"
in 60..69 -> "Below average"
in 0..59 -> "Fail"
else -> "Invalid score"
}
}
println(gradeDescription(85)) // Good
println(gradeDescription(55)) // Fail
when with Type Checks
fun describe(value: Any): String {
return when (value) {
is String -> "String with length ${value.length}"
is Int -> "Integer: $value"
is Double -> "Double: $value"
is Boolean -> if (value) "True" else "False"
is List<*> -> "List with ${value.size} items"
else -> "Unknown type"
}
}
println(describe(42)) // Integer: 42
println(describe("Hello")) // String with length 5
println(describe(true)) // True
After is String, Kotlin smart casts value to String, so you can use .length directly.
when Without an Argument
When used without an argument, when works like an if-else chain. Each branch is a Boolean condition:
fun classifyNumber(number: Int): String {
return when {
number < 0 -> "Negative"
number == 0 -> "Zero"
number % 2 == 0 -> "Positive even"
else -> "Positive odd"
}
}
println(classifyNumber(7)) // Positive odd
println(classifyNumber(-3)) // Negative
println(classifyNumber(0)) // Zero
println(classifyNumber(4)) // Positive even
Guard Conditions in when (Kotlin 2.2+)
Starting with Kotlin 2.2, you can add guard conditions to when branches using the if keyword. This lets you check additional conditions after the pattern match:
sealed class Status {
data class Success(val data: String) : Status()
data class Error(val code: Int, val message: String) : Status()
data object Loading : Status()
}
fun handleStatus(status: Status): String {
return when (status) {
is Status.Error if status.code == 404 -> "Not found: ${status.message}"
is Status.Error if status.code >= 500 -> "Server error: ${status.message}"
is Status.Error -> "Error ${status.code}: ${status.message}"
is Status.Success -> "OK: ${status.data}"
is Status.Loading -> "Loading..."
}
}
Without guard conditions, you would need nested if statements or separate when expressions. Guard conditions keep the code flat and readable.
Another example with a simpler type:
fun classifyScore(score: Int): String {
return when (score) {
in 90..100 if score == 100 -> "Perfect score!"
in 90..100 -> "Excellent"
in 80..89 -> "Good"
else -> "Keep studying"
}
}
when as a Statement
You can also use when without returning a value:
fun printSeason(month: Int) {
when (month) {
12, 1, 2 -> println("Winter")
3, 4, 5 -> println("Spring")
6, 7, 8 -> println("Summer")
9, 10, 11 -> println("Autumn")
else -> println("Invalid month")
}
}
Ranges
Ranges define a sequence of values. They are used in loops, when expressions, and if checks.
// Inclusive range — 1, 2, 3, 4, 5
val oneToFive = 1..5
// Exclusive end — 1, 2, 3, 4
val oneToFour = 1..<5
// Descending range — 5, 4, 3, 2, 1
val fiveToOne = 5 downTo 1
// With step — 0, 2, 4, 6, 8, 10
val evens = 0..10 step 2
// Descending with step — 10, 7, 4, 1
val countdown = 10 downTo 0 step 3
// Character range — a, b, c, d, e
val letters = 'a'..'e'
Check if a value is in a range:
val age = 25
println(age in 18..65) // true
println(age !in 0..17) // true
val grade = 'B'
println(grade in 'A'..'F') // true
for Loops
Iterate Over a Range
for (i in 1..5) {
print("$i ") // 1 2 3 4 5
}
Iterate with Step
for (i in 0..10 step 2) {
print("$i ") // 0 2 4 6 8 10
}
Iterate Backwards
for (i in 5 downTo 1) {
print("$i ") // 5 4 3 2 1
}
Iterate Over a Collection
val fruits = listOf("Apple", "Banana", "Cherry")
for (fruit in fruits) {
println("I like $fruit")
}
Iterate with Index
val fruits = listOf("Apple", "Banana", "Cherry")
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit")
}
// 0: Apple
// 1: Banana
// 2: Cherry
Iterate Over a String
for (char in "Kotlin") {
print("$char ") // K o t l i n
}
Iterate Over a Map
val scores = mapOf("Alex" to 95, "Sam" to 87, "Jordan" to 92)
for ((name, score) in scores) {
println("$name scored $score")
}
repeat
For simple repetition, use repeat:
repeat(3) { index ->
println("Repeat $index")
}
// Repeat 0
// Repeat 1
// Repeat 2
while and do-while
while Loop
while runs as long as the condition is true:
var count = 0
var sum = 0
while (sum < 100) {
sum += count
count++
}
println("Sum reached $sum after $count iterations")
do-while Loop
do-while runs at least once, then checks the condition:
val numbers = mutableListOf<Int>()
var i = 1
do {
numbers.add(i)
i *= 2
} while (i <= 100)
println(numbers) // [1, 2, 4, 8, 16, 32, 64]
The difference: while might not run at all if the condition is false from the start. do-while always runs at least once.
break and continue
break — Exit the Loop
for (i in 1..100) {
if (i * i > 50) {
println("$i (because $i * $i = ${i * i})")
break // Exit the loop
}
}
// 8 (because 8 * 8 = 64)
continue — Skip to Next Iteration
for (i in 1..10) {
if (i % 2 == 0) continue // Skip even numbers
print("$i ") // 1 3 5 7 9
}
Labels
Labels let you break or continue from a specific loop when you have nested loops.
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i == 2 && j == 2) {
println("Breaking at i=$i, j=$j")
break@outer // Breaks the OUTER loop
}
println("i=$i, j=$j")
}
}
Output:
i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
Breaking at i=2, j=2
Without @outer, break would only exit the inner loop. With break@outer, it exits both loops.
Practical Example: FizzBuzz
FizzBuzz is a classic programming exercise that uses all the control flow concepts:
fun fizzBuzz(n: Int): List<String> {
val result = mutableListOf<String>()
for (i in 1..n) {
val value = when {
i % 15 == 0 -> "FizzBuzz"
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
else -> i.toString()
}
result.add(value)
}
return result
}
println(fizzBuzz(15))
// [1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz]
Practical Example: Prime Numbers
fun isPrime(n: Int): Boolean {
if (n < 2) return false
for (i in 2..n / 2) {
if (n % i == 0) return false
}
return true
}
fun nextPrime(after: Int): Int {
var candidate = after + 1
while (!isPrime(candidate)) {
candidate++
}
return candidate
}
println(isPrime(7)) // true
println(isPrime(10)) // false
println(nextPrime(10)) // 11
println(nextPrime(20)) // 23
Summary
| Feature | Description |
|---|---|
if/else | Expression that returns a value |
when(value) | Match on values, ranges, types |
when {} | Condition-based matching (no argument) |
1..5 | Inclusive range |
1..<5 | Exclusive end range |
5 downTo 1 | Descending range |
step 2 | Skip values in a range |
for (i in range) | Iterate over range/collection |
while (cond) | Loop while condition is true |
do { } while | Loop at least once |
break | Exit loop |
continue | Skip to next iteration |
break@label | Exit labeled loop |
is Type if cond | Guard condition in when (Kotlin 2.2+) |
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
In the next tutorial, Kotlin Tutorial #7: Classes, Objects, and Data Classes, you will learn:
- How to create classes in Kotlin
- Constructors and properties
- Inheritance and interfaces
- Data classes
- Objects and companion objects
- Sealed classes
This is part 6 of the Kotlin Tutorial series. Check out Part 5: Functions if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.