Free Handbook · Every example compiled & verified

Flow Control

if and when as expressions, for loops over ranges with until, downTo and step, while and do-while, labeled break and continue, and repeat.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 02 · what you'll be able to do

  • Use if as an expression that returns a value, and explain why Kotlin has no ternary operator
  • Write when with values, ranges, type checks and no subject, and make it exhaustive when it returns a value
  • Loop over ranges and progressions with .., until, downTo and step, and over indices with withIndex()
  • Choose between for, while, do-while and repeat for a given loop
  • Leave or skip iterations of nested loops with labeled break and continue
01

if as an expression

The statement form of if looks like every C-family language: a condition in parentheses, a block, optional else if and else. The difference is that in Kotlin if is also an expression — it produces a value you can assign, return or pass to a function. The value of each branch is its last expression. That is why Kotlin has no condition ? a : b ternary operator: if (condition) a else b already does the job and reads as English.

kotlinMain.kt
fun main() {
    val temperature = 31

    // statement form
    if (temperature > 30) {
        println("Hot day")
    } else if (temperature > 20) {
        println("Warm day")
    } else {
        println("Cool day")
    }

    // expression form: the chosen branch is the value
    val advice = if (temperature > 30) "Drink water" else "Enjoy"
    println(advice)

    val max = if (temperature > 25) {
        println("checking a block branch")
        temperature      // last expression = the value
    } else {
        25
    }
    println("max = $max")
}
Outputcompiled & run with real Kotlin
Hot day
Drink water
checking a block branch
max = 31
Your turn

Set temperature to 18 and predict all four lines before you run it.

Error you will hit

if used as an expression without else

kotlin
fun main() {
    val n = 5
    val label = if (n > 0) "positive"
    println(label)
}
Main.kt:3:17: error: 'if' must have both main and 'else' branches when used as an expression.
Why the compiler said that

A value must exist on every path. If n were negative, the if would have nothing to produce, and label would have no value. The compiler refuses to guess.

The fix

Add an else branch. If there genuinely is no value in the other case, make the variable nullable and write else null.

kotlin
fun main() {
    val n = 5
    val label = if (n > 0) "positive" else "not positive"
    println(label)
}
Prefer a val assigned from if
Beginners often write var x = "" and then set it inside each branch. val x = if (…) a else b is shorter, cannot be forgotten on one path, and keeps x read-only.
02

when: Kotlin's switch, only better

when replaces switch. It checks its subject against each branch from top to bottom and runs the first match only — there is no fall-through and no break. A branch condition can be a value, several values separated by commas, a range with in, or a type check with is. else catches everything else.

kotlinMain.kt
fun describe(x: Any): String = when (x) {
    0 -> "zero"
    1, 2, 3 -> "small number"
    in 4..9 -> "single digit"
    is Int -> "a bigger Int"
    is String -> "text of length ${x.length}"   // smart cast: x is a String here
    else -> "something else"
}

fun main() {
    println(describe(0))
    println(describe(2))
    println(describe(7))
    println(describe(250))
    println(describe("hello"))
    println(describe(4.5))
}
Outputcompiled & run with real Kotlin
zero
small number
single digit
a bigger Int
text of length 5
something else

After is String -> the compiler treats x as a String inside that branch, so x.length compiles without a cast.

Your turn

Add a branch is Double -> "a decimal" above else. Which call changes its output?

Leave the subject out and each branch becomes a plain boolean condition. This is the idiomatic replacement for a long if / else if chain: the conditions line up and the first true one wins.

kotlinMain.kt
fun grade(score: Int): String = when {
    score !in 0..100 -> "invalid"
    score >= 90 -> "A"
    score >= 75 -> "B"
    score >= 50 -> "C"
    else -> "F"
}

fun main() {
    for (s in listOf(95, 75, 60, 12, 140)) {
        println("$s -> ${grade(s)}")
    }
}
Outputcompiled & run with real Kotlin
95 -> A
75 -> B
60 -> C
12 -> F
140 -> invalid
Your turn

Move the score >= 50 branch to the top. Why does every valid score now get a C?

Error you will hit

when used as an expression is not exhaustive

kotlin
fun main() {
    val code = 404
    val text = when (code) {
        200 -> "OK"
        404 -> "Not Found"
    }
    println(text)
}
Main.kt:3:16: error: 'when' expression must be exhaustive. Add an 'else' branch.
Why the compiler said that

When a when produces a value, every possible subject must be covered. An Int has four billion values and only two are handled. (For an enum or a sealed type, listing every case is enough — Module 08 shows that.)

The fix

Add an else branch that says what happens to everything else.

kotlin
fun main() {
    val code = 404
    val text = when (code) {
        200 -> "OK"
        404 -> "Not Found"
        else -> "Unknown"
    }
    println(text)
}
when shines with sealed types
In real Android and backend code, the most common when is over a sealed result type: is Loading ->, is Success ->, is Error ->. Without an else, adding a new subtype breaks the build everywhere it is not handled — which is exactly what you want. See Module 08.
03

for loops over ranges and collections

Kotlin has no C-style for (int i = 0; i < n; i++). A for loop walks anything that can be iterated: a range, a string, a list, a map. The loop variable is a fresh read-only value on each pass. Use .. for an inclusive range, until (or ..<) to stop before the end, downTo to count backwards and step to skip.

kotlinMain.kt
fun main() {
    for (i in 1..3) print("$i ")
    println()

    for (i in 0 until 3) print("$i ")
    println()

    for (i in 10 downTo 0 step 5) print("$i ")
    println()

    for (ch in "abc") print("$ch-")
    println()

    val fruits = listOf("apple", "banana", "cherry")
    for (i in fruits.indices) println("$i: ${fruits[i]}")
    for ((index, fruit) in fruits.withIndex()) println("#${index + 1} $fruit")

    val stock = mapOf("pens" to 12, "pads" to 3)
    for ((item, count) in stock) println("$item=$count")
}
Outputcompiled & run with real Kotlin
1 2 3 
0 1 2 
10 5 0 
a-b-c-
0: apple
1: banana
2: cherry
#1 apple
#2 banana
#3 cherry
pens=12
pads=3

(index, fruit) and (item, count) are destructuring declarations — they unpack a pair of values in one step.

Your turn

Print the odd numbers from 15 down to 1 on one line using downTo and step.

VisualizeSumming with a for loopStep 1 / 7
fun main() {
var total = 0
for (n in 1..4) {
total += n
}
println(total)
}
Line 2

total starts at 0. It is a var because it changes on every pass.

Variables now
total0
All 7 steps as a table
StepLineWhat happenedVariables now
12total starts at 0. It is a var because it changes on every pass.total = 0
23The range 1..4 hands the loop its first value.n = 1
340 + 1.total = 1
44Next value from the range: n is 2, so 1 + 2.n = 2 total = 3
54n is 3: 3 + 3.n = 3 total = 6
64n is 4, the last value — .. includes the end.n = 4 total = 10
76The range is exhausted, the loop ends, and the total is printed.
Error you will hit

A negative step

kotlin
fun main() {
    for (i in 10 downTo 0 step -2) {
        println(i)
    }
}
Exception in thread "main" java.lang.IllegalArgumentException: Step must be positive, was: -2.
	at MainKt.main(Main.kt:2)
	at MainKt.main(Main.kt)
Why the compiler said that

This compiles, then fails at runtime. The direction of a progression comes from downTo, not from the sign of the step, so step only accepts a positive number.

The fix

Keep downTo for the direction and give step the size of each jump: 10 downTo 0 step 2.

kotlin
fun main() {
    for (i in 10 downTo 0 step 2) {
        println(i)
    }
}
10..1 is empty, not backwards
for (i in 10..1) compiles and runs zero times, silently. A range only counts up. To count down, write 10 downTo 1.
04

while and do-while

Use for when you know what you are iterating over. Use while when you loop until a condition changes and you cannot say in advance how many passes that takes. do { } while (condition) checks the condition after the body, so the body always runs at least once — handy for "ask, then check" input loops. A variable declared inside a do block is visible in its while condition.

kotlinMain.kt
fun main() {
    // How many times can 1000 be halved before it drops below 1?
    var n = 1000.0
    var halvings = 0
    while (n >= 1) {
        n /= 2
        halvings++
    }
    println("halvings: $halvings")

    // Collatz: keep going until we reach 1
    var x = 6
    val path = mutableListOf(x)
    while (x != 1) {
        x = if (x % 2 == 0) x / 2 else 3 * x + 1
        path.add(x)
    }
    println(path)

    // do-while runs the body once even though the condition is already false
    var tries = 10
    do {
        println("attempt $tries")
        tries++
    } while (tries < 3)
}
Outputcompiled & run with real Kotlin
halvings: 10
[6, 3, 10, 5, 16, 8, 4, 2, 1]
attempt 10
Your turn

Change the Collatz start to 7 and count the steps with path.size - 1.

kotlinMain.kt
fun main() {
    // Re-ask until the input is a valid number between 1 and 5
    var rating: Int?
    do {
        print("Rate 1-5: ")
        val line = readln()
        rating = line.toIntOrNull()?.takeIf { it in 1..5 }
        if (rating == null) println("'$line' is not valid")
    } while (rating == null)
    println("Thanks for rating $rating")
}
Outputcompiled & run with real Kotlin
Rate 1-5: 'ten' is not valid
Rate 1-5: '9' is not valid
Rate 1-5: Thanks for rating 4

Run with the three stdin lines ten, 9, 4. takeIf keeps the number only when the condition holds, otherwise gives null.

The infinite loop
If nothing inside a while body can make the condition false, the program never ends. Before you run a while, point at the line that moves it toward stopping.
05

break, continue and labels

break leaves the innermost loop immediately. continue skips the rest of the current pass and moves to the next one. In nested loops, both act on the inner loop only. To target an outer loop, give it a label — a name followed by @, such as outer@ — and write break@outer or continue@outer.

kotlinMain.kt
fun main() {
    for (n in 1..10) {
        if (n % 3 == 0) continue   // skip multiples of 3
        if (n > 7) break           // stop completely after 7
        print("$n ")
    }
    println()

    val grid = listOf(
        listOf(4, 8, 15),
        listOf(16, 23, 42),
        listOf(7, 1, 9),
    )
    val target = 23
    outer@ for ((r, row) in grid.withIndex()) {
        for ((c, value) in row.withIndex()) {
            if (value == target) {
                println("found $target at row $r, col $c")
                break@outer          // leave BOTH loops
            }
        }
    }
    println("search done")
}
Outputcompiled & run with real Kotlin
1 2 4 5 7 
found 23 at row 1, col 1
search done
Your turn

Replace break@outer with a plain break and add a println("row $r") at the end of the outer loop body. What changes?

Visualizecontinue@outer skips to the next rowStep 1 / 6
fun main() {
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) continue@outer
println("$i,$j")
}
}
}
Line 2

The outer loop is labeled outer@ and starts with i = 1.

Variables now
i1
All 6 steps as a table
StepLineWhat happenedVariables now
12The outer loop is labeled outer@ and starts with i = 1.i = 1
25Inner loop, j = 1: the check is false, so the pair is printed.j = 1
34j = 2: continue@outer abandons the inner loop and jumps to the next pass of the outer loop. j = 3 never runs for this row.j = 2
45i = 2, the inner loop starts again at j = 1.i = 2 j = 1
54j = 2 again jumps to the outer loop.j = 2
65i = 3, j = 1 is printed; j = 2 jumps out once more, and the outer range is finished.i = 3 j = 1
Labels are a smell in long functions
A labeled break across three nested loops is legitimate in a grid search. In business code, moving the nested loops into a small function and using return is usually clearer — and the function gets a name that explains what it finds.
06

Ranges, progressions and repeat

Behind every loop header above is an object. 1..10 is an IntRange; add step or downTo and you get an IntProgression with a first, a last and a step. Because they are objects, you can store them in variables, pass them to functions and call collection functions such as sum(), filter, map or reversed() on them. When you only need to do something n times, repeat(n) { } says so directly; it is the 0-based pass number.

kotlinMain.kt
fun main() {
    val evens = 2..20 step 4
    println("first=${evens.first} last=${evens.last} step=${evens.step}")
    println(evens.toList())

    val working = 9..17
    println(13 in working)
    println(working.count())
    println((1..10).filter { it % 2 == 1 })
    println((1..5).reversed().toList())
    println((1..5).map { it * it })
    println(('a'..'z' step 5).joinToString(""))

    repeat(3) { println("line ${it + 1}") }
    println("-".repeat(10))
}
Outputcompiled & run with real Kotlin
first=2 last=18 step=4
[2, 6, 10, 14, 18]
true
9
[1, 3, 5, 7, 9]
[5, 4, 3, 2, 1]
[1, 4, 9, 16, 25]
afkpuz
line 1
line 2
line 3
----------

last is 18, not 20: a progression's last element is the last value its step actually lands on. "-".repeat(10) is a different function — the String one.

Your turn

Print a 3 by 4 rectangle of * characters using repeat twice, nested.

You wantWriteValues
1 to 5, both ends1..51 2 3 4 5
0 up to, not including, n0 until n or 0..<n0 … n-1
Every index of a listlist.indices0 … size-1
Backwards5 downTo 15 4 3 2 1
Skip values0..10 step 50 5 10
Last index backwardslist.indices.reversed()size-1 … 0
Just n timesrepeat(n) { }it = 0 … n-1
Expression
Code that produces a value. In Kotlin, if and when are expressions, so they can be assigned or returned.
when
Kotlin's multi-way branch: matches values, ranges (in), types (is) or plain conditions; the first matching branch wins, with no fall-through.
Exhaustive
Covering every possible input. A when used as a value must be exhaustive — usually by adding else.
Progression
A range with a step, such as 10 downTo 0 step 2. It has first, last and step properties.
until / ..<
Builds a range that excludes its upper end.
downTo
Builds a progression that counts backwards. step must still be positive.
Label
A name like outer@ placed before a loop so break@outer or continue@outer can target it.
repeat(n)
Standard-library function that runs a lambda n times, with it = 0 to n-1.
Quick check

What does for (i in 5..1) println(i) print?

Quick check

In when (x) { 1 -> a(); 1, 2 -> b(); else -> c() } with x = 1, what runs?

Frequently asked questions

Does Kotlin have a ternary operator?
No. if is an expression in Kotlin, so val max = if (a > b) a else b does what a > b ? a : b does in Java, and reads more clearly. For null checks, the Elvis operator ?: covers the other common use of a ternary.
What is the difference between when and switch?
when has no fall-through and needs no break; it can match several values, ranges, types and arbitrary conditions; and it is an expression that returns a value. When used as a value, the compiler checks that it is exhaustive.
How do I write a classic for (int i = 0; i < n; i++) loop in Kotlin?
Write for (i in 0 until n) (or 0..<n). For list indexes use list.indices, and for both index and value use list.withIndex(). If you only need to run something n times, repeat(n) { } is shortest.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.