Loops #
Loops are the mechanism for executing a code block repeatedly — either a predetermined number of times, or while a certain condition holds. Kotlin provides three classic loop keywords: for, while, and do-while. But Kotlin also encourages a more declarative approach through higher-order functions like forEach, map, filter, and reduce — which are often more expressive and safer from common manual-loop errors like off-by-one bugs. This article covers all loop forms in Kotlin, when to use each one, and patterns that make looping code cleaner.
The for Loop
#
for in Kotlin iterates over anything implementing Iterable — ranges, arrays, lists, maps, strings, and more. There’s no for (i = 0; i < n; i++) form like in Java or C — Kotlin replaces it with more declarative syntax.
Range Iteration #
// Inclusive at both ends: 1, 2, 3, 4, 5
for (i in 1..5) {
print("$i ")
}
// 1 2 3 4 5
// Exclusive at the right end: 0, 1, 2, 3, 4
for (i in 0 until 5) {
print("$i ")
}
// 0 1 2 3 4
// Backwards: 5, 4, 3, 2, 1
for (i in 5 downTo 1) {
print("$i ")
}
// 5 4 3 2 1
// With a step: 0, 2, 4, 6, 8, 10
for (i in 0..10 step 2) {
print("$i ")
}
// 0 2 4 6 8 10
// Backwards with a step: 10, 7, 4, 1
for (i in 10 downTo 1 step 3) {
print("$i ")
}
// 10 7 4 1
Collection Iteration #
val languages = listOf("Kotlin", "Java", "Python", "Go")
// Element iteration
for (lang in languages) {
println(lang)
}
// Iteration with index using withIndex()
for ((index, lang) in languages.withIndex()) {
println("[$index] $lang")
}
// [0] Kotlin
// [1] Java
// [2] Python
// [3] Go
// Index-only iteration if elements aren't needed
for (i in languages.indices) {
println("Position $i: ${languages[i]}")
}
Map Iteration #
val capitals = mapOf(
"Indonesia" to "Jakarta",
"Japan" to "Tokyo",
"France" to "Paris",
"Brazil" to "Brasilia"
)
// Destructure map entries directly in for
for ((country, city) in capitals) {
println("$country → $city")
}
// If you only need keys or values
for (country in capitals.keys) print("$country ")
for (city in capitals.values) print("$city ")
String Iteration #
String can also be iterated character by character:
val word = "Kotlin"
for (character in word) {
print("$character-")
}
// K-o-t-l-i-n-
// With index
for ((i, c) in word.withIndex()) {
println("[$i] = '$c'")
}
The while Loop
#
while evaluates the condition before each iteration. If the condition is false from the start, the code block is never executed at all.
var count = 1
while (count <= 5) {
println("Iteration $count")
count++
}
while is most appropriate when the number of iterations isn’t known in advance and depends on a dynamic condition:
// Connection attempt simulation
var attempts = 0
val maxAttempts = 3
var connected = false
while (!connected && attempts < maxAttempts) {
attempts++
println("Connection attempt $attempts...")
connected = tryConnect() // a function returning Boolean
if (!connected && attempts < maxAttempts) {
println("Failed, retrying in 2 seconds...")
Thread.sleep(2_000)
}
}
if (connected) {
println("Connected successfully!")
} else {
println("Connection failed after $maxAttempts attempts.")
}
Infinite Loop with while
#
// Loop forever — there must be an exit mechanism (break or return)
while (true) {
val input = readInput()
if (input == "quit") break
processInput(input)
}
The do-while Loop
#
do-while evaluates the condition after each iteration. This guarantees the code block executes at least once, even if the condition is already false from the start.
var i = 10
do {
println("Value of i: $i")
i++
} while (i <= 5)
// Output: "Value of i: 10" — executes once even though the condition is immediately false
The most classic do-while use case is asking for user input until the input is valid:
var input: String
var number: Int
do {
print("Enter a number between 1 and 10: ")
input = readLine() ?: ""
number = input.toIntOrNull() ?: -1
if (number !in 1..10) {
println("Invalid input. Try again.")
}
} while (number !in 1..10)
println("You entered: $number")
This pattern is more natural than while because you don’t need to initialize the variable with a dummy value before the loop just to make the first condition evaluable.
break and continue
#
break — Exit the Loop
#
break stops the loop entirely and continues execution to the code after the loop:
val list = listOf(3, 7, 2, 9, 1, 5, 8, 4)
var target = 9
var position = -1
for ((index, value) in list.withIndex()) {
if (value == target) {
position = index
break // no need to continue after finding it
}
}
if (position >= 0) {
println("$target found at index $position")
} else {
println("$target not found")
}
continue — Skip This Iteration
#
continue skips the rest of the code in the current iteration and moves straight to the next one:
val numbers = listOf(1, -3, 5, -2, 8, -7, 4)
print("Positive numbers: ")
for (n in numbers) {
if (n < 0) continue // skip negative numbers
print("$n ")
}
// Positive numbers: 1 5 8 4
Labels — break and continue in Nested Loops
#
By default, break and continue only affect the innermost loop. To affect an outer loop, use labels.
// ANTI-PATTERN: trying to exit an outer loop with a boolean flag
var found = false
for (i in 1..5) {
for (j in 1..5) {
if (i * j == 12) {
found = true
break // only exits the inner loop!
}
}
if (found) break // needs a second break for the outer loop
}
// CORRECT: use a label to exit the outer loop directly
outerLoop@ for (i in 1..5) {
for (j in 1..5) {
if (i * j == 12) {
println("Found: $i × $j = 12")
break@outerLoop // exits the labeled loop directly
}
}
}
// Found: 3 × 4 = 12
Labels also work with continue:
outerLoop@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) continue@outerLoop // go to the next iteration of the outer loop
println("i=$i, j=$j")
}
}
// i=1, j=1
// i=2, j=1
// i=3, j=1
repeat — Simple Iteration a Set Number of Times
#
For repeating something N times without needing a counter variable, repeat is the cleanest choice:
// ANTI-PATTERN: for used only for counting
for (i in 1..5) {
println("Hello!")
}
// CORRECT: repeat if you don't need the counter value
repeat(5) {
println("Hello!")
}
// repeat provides the index if needed
repeat(5) { index ->
println("Iteration $index")
}
// Iteration 0
// Iteration 1
// ...
// Iteration 4
Nested Loops #
Loops inside loops are useful for working with two-dimensional data structures like matrices, tables, or combinations.
// Multiplication table
for (i in 1..5) {
for (j in 1..5) {
print("%4d".format(i * j))
}
println()
}
// 1 2 3 4 5
// 2 4 6 8 10
// 3 6 9 12 15
// 4 8 12 16 20
// 5 10 15 20 25
// Finding element pairs whose sum equals a target
val numbers = listOf(1, 3, 5, 7, 9)
val target = 10
for (i in numbers.indices) {
for (j in i + 1 until numbers.size) {
if (numbers[i] + numbers[j] == target) {
println("${numbers[i]} + ${numbers[j]} = $target")
}
}
}
// 1 + 9 = 10
// 3 + 7 = 10
Nested loops more than two levels deep are usually a sign the code needs refactoring — extract the inner loop into its own function, or consider a different approach. Nested complexity makes code hard to read and understand.
Functional Iteration — A More Expressive for Alternative
#
Kotlin encourages using higher-order functions as an alternative to manual loops for common collection operations. The functional approach is usually more expressive, safer from off-by-one errors, and easier to combine.
flowchart TD
A[Loop Requirement] --> B{Purpose?}
B -- Do something\nfor each element --> C["forEach { }"]
B -- Transform\neach element --> D["map { }"]
B -- Filter elements --> E["filter { }"]
B -- Accumulate\ninto one value --> F["reduce { } / fold { }"]
B -- Find one element --> G["find { } / first { }"]
B -- Check a condition\nacross all elements --> H["all { } / any { } / none { }"]
B -- Need the index\nin each iteration --> I["forEachIndexed { }"]
B -- Loop N times\nwithout a collection --> J["repeat(N) { }"]
B -- Unknown\ndynamic condition --> K["while / do-while"]forEach and forEachIndexed
#
val students = listOf("Budi", "Sari", "Ahmad", "Rina")
// forEach — without index
students.forEach { name ->
println("Hello, $name!")
}
// forEachIndexed — with index
students.forEachIndexed { i, name ->
println("${i + 1}. $name")
}
// 1. Budi
// 2. Sari
// 3. Ahmad
// 4. Rina
map — Transform Every Element
#
val prices = listOf(50_000, 75_000, 120_000, 30_000)
// ANTI-PATTERN: manual loop for transformation
val discountedPrices = mutableListOf<Int>()
for (p in prices) {
discountedPrices.add((p * 0.8).toInt())
}
// CORRECT: map is more concise and safer
val discountedPrices = prices.map { (it * 0.8).toInt() }
println(discountedPrices) // [40000, 60000, 96000, 24000]
filter — Filtering Elements
#
val scores = listOf(55, 78, 90, 42, 85, 67, 91, 38)
// Manual loop
val passed = mutableListOf<Int>()
for (n in scores) {
if (n >= 70) passed.add(n)
}
// Idiomatic
val passed = scores.filter { it >= 70 }
println(passed) // [78, 90, 85, 91]
Combining Operations — Method Chaining #
The real power of the functional approach is how easily operations combine:
data class Product(val name: String, val price: Int, val stock: Int)
val products = listOf(
Product("Laptop", 15_000_000, 5),
Product("Mouse", 250_000, 0),
Product("Keyboard", 500_000, 12),
Product("Monitor", 4_000_000, 3),
Product("Headset", 800_000, 0),
)
// Show available products, sorted from cheapest, showing name and price
val available = products
.filter { it.stock > 0 }
.sortedBy { it.price }
.map { "${it.name}: Rp${\"%,d\".format(it.price)}" }
available.forEach { println(it) }
// Keyboard: Rp500,000
// Monitor: Rp4,000,000
// Laptop: Rp15,000,000
Compare with the equivalent manual loop — far more code and more bug-prone:
// ANTI-PATTERN: manual loop for the same thing
val available = mutableListOf<Product>()
for (p in products) {
if (p.stock > 0) available.add(p)
}
available.sortBy { it.price }
val result = mutableListOf<String>()
for (p in available) {
result.add("${p.name}: Rp${\"%,d\".format(p.price)}")
}
for (s in result) println(s)
reduce and fold — Accumulation
#
val numbers = listOf(1, 2, 3, 4, 5)
// reduce: accumulation starts from the first element
val sum = numbers.reduce { acc, value -> acc + value }
println(sum) // 15
val product = numbers.reduce { acc, value -> acc * value }
println(product) // 120
// fold: accumulation with an initial value
val sumWithOffset = numbers.fold(100) { acc, value -> acc + value }
println(sumWithOffset) // 115
// sumOf — specifically for sums (more expressive than reduce)
val total = numbers.sumOf { it }
println(total) // 15
Choosing the Right Loop Type #
| Situation | Best Choice |
|---|---|
| Iterate a collection/range without modification | for or forEach |
| Transform every element | map |
| Filter elements | filter |
| Accumulate into one value | reduce / fold / sumOf |
| Iterate with an index | forEachIndexed or for + withIndex() |
| Repeat N times without a counter | repeat(N) |
| Dynamic condition, uncertain iteration count | while |
| At least one execution, then check the condition | do-while |
| Early exit from nested loops | for + label + break@label |
Summary #
forin Kotlin is for-each — there’s nofor (i=0; i<n; i++)form. Use ranges (1..5,0 until n),withIndex(), orindicesfor the same purposes.whilefor dynamic conditions — choosewhilewhen the iteration count isn’t known in advance and depends on a condition that changes during execution.do-whileguarantees at least one execution — useful for the “try first, check later” pattern, like asking for user input until it’s valid.repeat(N)for fixed-count repetition — cleaner thanfor (i in 1..N)when the counter value isn’t needed.- Labels for
break/continuein nested loops —break@labelorcontinue@labelaffects the labeled loop, not just the innermost one.- Prefer the functional approach for collections —
forEach,map,filter,reduce, and their combinations are more expressive, safer, and easier to compose than manual loops.- Avoid nested loops more than two levels deep — extract into functions or consider a different approach. Three levels of nesting can almost always be simplified.
mapisn’t for side effects — useforEachif the goal is an action (printing, saving to DB). Usemapif the goal is producing a new collection from a transformation.