Loops #
Loops are the mechanism for executing a block of code more than once. Dart provides four imperative constructs (for, for-in, while, do-while) and a set of functional methods (map, where, reduce, fold), each with different characteristics. Choosing the right one isn’t just about “they can all do the same thing” — each construct communicates a different intent to the code’s readers. The classic for says “I need the index”, for-in says “I only need the elements”, and map says “I’m transforming this collection”. This article covers when to use each, anti-patterns to avoid, and rarely discussed topics like asynchronous loops.
The Classic for
#
for with three components (initialization; condition; update) is the most explicit form of looping. Use it when you need index access, or when the increment logic isn’t standard.
// Basic structure
for (int i = 0; i < 5; i++) {
print('Iteration $i');
}
// Output: Iteration 0, Iteration 1, ... Iteration 4
// Backward iteration
for (int i = 10; i >= 0; i -= 2) {
print(i); // 10, 8, 6, 4, 2, 0
}
// Initialization outside — the variable stays accessible after the loop
int i = 0;
for (; i < 5; i++) {
if (i == 3) break;
}
print(i); // 3 — still accessible
Accessing Index and Element Together #
One of the main reasons to choose the classic for is the need for an index:
List<String> produk = ['Laptop', 'Mouse', 'Keyboard', 'Monitor'];
// Use classic for when you need the index
for (int i = 0; i < produk.length; i++) {
print('${i + 1}. ${produk[i]}'); // "1. Laptop", "2. Mouse", etc
}
// Idiomatic alternative: asMap() + for-in
for (final entry in produk.asMap().entries) {
print('${entry.key + 1}. ${entry.value}');
}
// Or with indexed() from Dart 3.x
for (final (i, item) in produk.indexed) {
print('${i + 1}. $item');
}
// ANTI-PATTERN: using classic for only to access elements
for (int i = 0; i < produk.length; i++) {
print(produk[i]); // i is only used for access — wasteful
}
// CORRECT: use for-in if you don't need the index
for (final p in produk) {
print(p); // cleaner and the intent is clear
}
for-in — Iterating Collections
#
for-in is the most idiomatic form of looping in Dart for iterating collection elements. It works with any object implementing Iterable — including List, Set, Map.entries, Map.keys, Map.values, and String (iterating per character).
// List
List<String> kota = ['Jakarta', 'Bandung', 'Surabaya', 'Medan'];
for (final k in kota) {
print(k);
}
// Set
Set<int> angkaPrima = {2, 3, 5, 7, 11};
for (final p in angkaPrima) {
print(p);
}
// Map — iterating entries
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
for (final entry in skor.entries) {
print('${entry.key}: ${entry.value}');
}
// Iterating only keys or only values
for (final nama in skor.keys) print(nama);
for (final nilai in skor.values) print(nilai);
// String — iterating per character
for (final karakter in 'Dart') {
print(karakter); // D, a, r, t
}
for-in with Generator Iterables
#
for-in also works with lazy iterables — collections evaluated one at a time as they’re iterated, not loaded entirely into memory:
// Range-like iteration with Iterable.generate
Iterable<int> range(int dari, int ke) sync* {
for (int i = dari; i < ke; i++) yield i;
}
for (final n in range(1, 6)) {
print(n); // 1, 2, 3, 4, 5
}
// Or use where/map on an existing iterable
for (final n in List.generate(100, (i) => i).where((n) => n.isEven).take(5)) {
print(n); // 0, 2, 4, 6, 8 — lazy, only takes the first 5 even numbers
}
forEach — Iteration Method
#
forEach is a method on Iterable that accepts a callback function and calls it for every element. Functionally similar to for-in, but with important limitations.
List<int> angka = [1, 2, 3, 4, 5];
// forEach with a lambda
angka.forEach((n) => print(n * 2));
// forEach with a function reference
angka.forEach(print); // same as (n) => print(n)
// Map.forEach — receives key and value
Map<String, int> skor = {'Budi': 90, 'Siti': 85};
skor.forEach((nama, nilai) => print('$nama: $nilai'));
forEach Limitations
#
// ANTI-PATTERN: using forEach when you need break or continue
List<int> angka = [1, 2, 3, 4, 5];
angka.forEach((n) {
if (n == 3) break; // ✗ error: can't break inside forEach
if (n == 2) continue; // ✗ error: can't continue inside forEach
print(n);
});
// CORRECT: use for-in if you need break or continue
for (final n in angka) {
if (n == 3) break; // ✓
if (n == 2) continue; // ✓
print(n);
}
// ANTI-PATTERN: forEach with async/await — doesn't wait properly
List<String> ids = ['U001', 'U002', 'U003'];
ids.forEach((id) async {
await hapusPengguna(id); // ✗ forEach doesn't await Futures — all run in parallel
});
// The program may finish before all deletions actually complete
// CORRECT: use for-in with async/await
for (final id in ids) {
await hapusPengguna(id); // ✓ waits one at a time
}
// Or if you really want parallel, use Future.wait explicitly
await Future.wait(ids.map((id) => hapusPengguna(id)));
while — Condition-Based Loops
#
while evaluates the condition before each iteration. Use it when the iteration count isn’t known upfront and the stopping condition depends on something that changes inside the loop.
// Reading until a condition is met
int nilaiInput = 0;
while (nilaiInput <= 0) {
nilaiInput = bacaInput(); // keeps looping until input is positive
}
// Traversing a hierarchical data structure
TreeNode? node = pohon.akar;
while (node != null && !node.adalahTarget) {
node = node.kiri ?? node.kanan;
}
// Polling with a timeout
final batas = DateTime.now().add(Duration(seconds: 30));
while (DateTime.now().isBefore(batas)) {
final hasil = cekStatus();
if (hasil == 'selesai') break;
sleep(Duration(seconds: 1));
}
Beware of Infinite Loops #
while is the only loop construct that allows accidental infinite loops if the stopping condition is never met:
// ANTI-PATTERN: a stopping condition that's never reached
int n = 1;
while (n > 0) {
n++; // n is always > 0 because it keeps increasing — infinite loop!
print(n);
}
// ANTI-PATTERN: forgetting to update the condition variable
int hitung = 0;
while (hitung < 10) {
print(hitung); // ✗ hitung never increases — infinite loop!
}
// CORRECT: make sure the stopping condition is definitely reached
int hitung = 0;
while (hitung < 10) {
print(hitung);
hitung++; // ✓ the condition will eventually become false
}
do-while — Executes at Least Once
#
do-while evaluates the condition after each iteration, guaranteeing the loop body runs at least once — even if the condition is already false from the start.
// Classic scenario: a menu shown at least once
String pilihan;
do {
tampilkanMenu();
pilihan = bacaInput();
} while (pilihan != 'keluar');
// Interactive input validation
int nilai;
do {
print('Enter a value between 1-100:');
nilai = int.parse(bacaInput());
} while (nilai < 1 || nilai > 100);
print('Valid value: $nilai');
// ANTI-PATTERN: using do-while when the condition might be immediately false
// and the first execution isn't wanted
do {
kirimNotifikasi(pengguna); // ✗ sent even though the user isn't active
} while (pengguna.aktif);
// CORRECT: check the condition first with while if the first execution isn't guaranteed
if (pengguna.aktif) {
do {
kirimNotifikasi(pengguna);
} while (pengguna.aktif && pengguna.butuhPengingat());
}
// Or more simply: use a regular while
while (pengguna.aktif) {
kirimNotifikasi(pengguna);
}
Comparing Loop Constructs #
flowchart TD
A{What's being iterated?} --> B{A collection / Iterable?}
B -- Yes --> C{Need an index\\nor break/continue?}
C -- Only elements,\\nno break needed --> D[for-in or\\nfunctional methods]
C -- Need an index --> E[classic for\\nor asMap\\nor indexed]
C -- Need break/continue --> F[for-in]
B -- No,\\nconditional loop --> G{Must the body\\nrun at least once?}
G -- Yes --> H[do-while]
G -- No --> I[while]| Construct | Best for | Can break/continue | Can async/await |
|---|---|---|---|
Classic for | Index-based iteration, custom increments | ✓ | ✓ |
for-in | Iterating collection elements | ✓ | ✓ |
forEach | Simple iteration without break | ✗ | ✗ (doesn’t work) |
while | Stopping condition unknown upfront | ✓ | ✓ |
do-while | Body must run at least once | ✓ | ✓ |
break and continue
#
break stops the loop entirely; continue skips the rest of the current iteration and goes straight to the next one. Both work in all imperative constructs (for, for-in, while, do-while).
List<int> angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// break — stop when the first element > 5 is found
for (final n in angka) {
if (n > 5) break;
print(n); // 1, 2, 3, 4, 5
}
// continue — skip odd numbers
for (final n in angka) {
if (n.isOdd) continue;
print(n); // 2, 4, 6, 8, 10
}
// break in a while — exit when a complex condition is met
int i = 0;
while (true) { // infinite loop controlled by break
i++;
if (i * i > 50) break;
}
print(i); // 8 — because 8² = 64 > 50
Labels for Nested Loops #
Labels allow break and continue to target an outer loop from inside an inner loop — very useful in nested loops:
// Without a label — break only exits the inner loop
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) break; // only exits the j loop
print('$i,$j');
}
}
// Output: 0,0 | 1,0 | 2,0
// With a label — break exits the outer loop
luarLoop:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) break luarLoop; // exits both
print('$i,$j');
}
}
// Output: 0,0 | 0,1 | 0,2 | 1,0
// continue with a label — skip to the next iteration of the outer loop
luarLoop:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) continue luarLoop; // skip the rest of the inner loop, go to next i
print('$i,$j');
}
}
// Output: 0,0 | 1,0 | 2,0
Labels make control flow non-linear and hard to trace. Before using a label, consider whether the logic could be factored into a separate function with return — which is far easier to read.The Functional Approach: map, where, reduce, fold
#
Dart supports a functional programming style for collection transformations. The functional approach is often more expressive and concise than imperative loops — especially for common operations like filtering, transforming, and aggregating.
map — Transforming Every Element
#
List<int> angka = [1, 2, 3, 4, 5];
// Imperative — verbose
List<int> hasilImperatif = [];
for (final n in angka) {
hasilImperatif.add(n * n);
}
// Functional — concise and expressive
List<int> hasilFungsional = angka.map((n) => n * n).toList();
// [1, 4, 9, 16, 25]
// map can be chained
List<String> diformat = angka
.map((n) => n * n) // square
.where((n) => n > 5) // filter > 5
.map((n) => 'nilai: $n') // format to String
.toList();
// ['nilai: 9', 'nilai: 16', 'nilai: 25']
where — Filtering Elements
#
List<Produk> produk = ambilSemuaProduk();
// Imperative
List<Produk> tersedia = [];
for (final p in produk) {
if (p.stok > 0 && p.aktif) {
tersedia.add(p);
}
}
// Functional
List<Produk> tersedia = produk
.where((p) => p.stok > 0 && p.aktif)
.toList();
reduce and fold — Aggregation
#
List<int> angka = [1, 2, 3, 4, 5];
// reduce — aggregation without an initial value (throws if the list is empty)
int jumlah = angka.reduce((acc, n) => acc + n); // 15
int maks = angka.reduce((a, b) => a > b ? a : b); // 5
// fold — aggregation with an initial value (safe for empty lists)
int jumlahFold = angka.fold(0, (acc, n) => acc + n); // 15
double rataRata = angka.fold(0, (acc, n) => acc + n) / angka.length; // 3.0
// fold for aggregating into a different type
String digabung = angka.fold('', (acc, n) => '$acc$n'); // '12345'
// fold to build a Map from a List
Map<String, int> panjangKata = ['halo', 'dart', 'pemrograman'].fold(
{},
(acc, kata) => acc..addAll({kata: kata.length}),
);
// {'halo': 4, 'dart': 4, 'pemrograman': 11}
Other Useful Aggregation Methods #
List<int> angka = [3, 1, 4, 1, 5, 9, 2, 6];
print(angka.any((n) => n > 8)); // true — there's one > 8
print(angka.every((n) => n > 0)); // true — all > 0
print(angka.contains(5)); // true
print(angka.indexOf(4)); // 2 — first index of value 4
print(angka.firstWhere((n) => n > 4)); // 5 — first element > 4
print(angka.lastWhere((n) => n < 4)); // 2 — last element < 4
print(angka.takeWhile((n) => n < 5).toList()); // [3, 1, 4, 1] — take while < 5
print(angka.skipWhile((n) => n < 5).toList()); // [5, 9, 2, 6] — skip while < 5
// Avoiding a firstWhere that throws when nothing is found
int? hasil = angka.firstWhereOrNull((n) => n > 100); // null, doesn't throw
Asynchronous Loops: await for
#
Dart supports asynchronous loops for Stream — sequences of data delivered over time. await for waits for each event from the stream before continuing to the next iteration.
// Stream from an async generator
Stream<int> hitungMundur(int dari) async* {
for (int i = dari; i >= 0; i--) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
// Iterating a stream with await for
Future<void> main() async {
await for (final angka in hitungMundur(5)) {
print(angka); // 5, 4, 3, 2, 1, 0 — one per second
}
print('Done!');
}
await for vs listen
#
// listen — non-blocking, doesn't wait for each event to finish processing
stream.listen((event) {
prosesEvent(event); // can overlap if prosesEvent is slow
});
// await for — blocking, waits for each iteration to finish
await for (final event in stream) {
await prosesEvent(event); // waits before the next event
}
// ANTI-PATTERN: using forEach on a Stream
stream.forEach((event) async {
await prosesEvent(event); // ✗ doesn't wait properly
});
// CORRECT: use await for to iterate a Stream with async
await for (final event in stream) {
await prosesEvent(event); // ✓ waits for each event
}
Nested Loops and Their Complexity #
Deep nested loops are one of the causes of slow, hard-to-read code. Each nesting level adds one dimension of complexity — two loops means O(n²), three loops means O(n³).
// O(n²) — acceptable for small n, watch out for large n
List<List<int>> matriks = [[1,2,3],[4,5,6],[7,8,9]];
for (final baris in matriks) {
for (final sel in baris) {
print(sel);
}
}
// ANTI-PATTERN: a triple nested loop that could be optimized
List<Toko> toko = ambilSemuaToko();
List<Produk> produkDitemukan = [];
for (final t in toko) { // O(n)
for (final k in t.kategori) { // O(m)
for (final p in k.produk) { // O(p) — total O(n*m*p)
if (p.harga < 50000) {
produkDitemukan.add(p);
}
}
}
}
// CORRECT: use a more expressive functional approach
List<Produk> produkDitemukan = toko
.expand((t) => t.kategori)
.expand((k) => k.produk)
.where((p) => p.harga < 50000)
.toList();
expand is the method that “flattens” one level of a collection — turning List<List<T>> into List<T>:
List<List<int>> nested = [[1,2], [3,4], [5,6]];
List<int> flat = nested.expand((list) => list).toList();
// [1, 2, 3, 4, 5, 6]
// Equivalent to flatMap in other languages
When to Choose Imperative vs Functional #
A question that often comes up: when should you use for-in vs map/where/reduce? The answer depends on the purpose of the operation:
USE the FUNCTIONAL approach (map, where, reduce, fold) when:
✓ Transforming a collection into a new collection
✓ Filtering elements by a condition
✓ Aggregating a collection into a single value
✓ Operations can be chained without temporary variables
✓ There are no side effects (not modifying external state)
USE the IMPERATIVE approach (for, for-in, while) when:
✓ You need break or continue to exit early
✓ There are side effects that need control (writing files, updating UI)
✓ Asynchronous iteration (await for, await inside a loop)
✓ The logic is too complex for a one-line lambda expression
✓ You need coordinated access to several variables outside the loop
Loop Anti-Patterns #
Modifying a Collection While Iterating #
List<int> angka = [1, 2, 3, 4, 5];
// ANTI-PATTERN: modifying a list during iteration — unpredictable behavior
for (final n in angka) {
if (n.isEven) angka.remove(n); // ✗ ConcurrentModificationError at runtime
}
// CORRECT: build a new list or iterate over a copy
// Option 1: filter into a new list
angka = angka.where((n) => n.isOdd).toList();
// Option 2: iterate from the back if in-place modification is required
for (int i = angka.length - 1; i >= 0; i--) {
if (angka[i].isEven) angka.removeAt(i);
}
Accumulating with + in a Loop
#
// ANTI-PATTERN: String concatenation with + in a loop — O(n²)
String hasil = '';
for (final kata in daftarKata) {
hasil += kata + ' '; // creates a new String object every iteration
}
// CORRECT: use StringBuffer — O(n)
final buffer = StringBuffer();
for (final kata in daftarKata) {
buffer.write(kata);
buffer.write(' ');
}
String hasil = buffer.toString();
// Or join for simple cases
String hasil = daftarKata.join(' ');
Loop Conditions Recomputed Every Iteration #
// ANTI-PATTERN: calling an expensive method in the loop condition every iteration
for (int i = 0; i < ambilPanjangDariDatabase(); i++) { // ✗ queries the DB every iteration
proses(i);
}
// CORRECT: compute once, store in a variable
final panjang = ambilPanjangDariDatabase();
for (int i = 0; i < panjang; i++) { // ✓ only one query
proses(i);
}
// For Lists this is already efficient because .length is O(1)
for (int i = 0; i < list.length; i++) { ... } // ✓ fine for List
Summary #
- Classic
forfor index-based iteration, non-standard increments, or access to several positions at once. UseasMap().entriesorindexedwhen you need both the index and the element.for-inis the most idiomatic way to iterate collections in Dart — use it always unless you need an index or a custom increment.forEachhas important limitations: nobreak, nocontinue, and no properasync/await. Use it only for simple iteration without control flow.whilefor stopping conditions unknown upfront — make sure the stopping condition is definitely reached to avoid infinite loops.do-whilefor bodies that must execute at least once — most common for interactive menus and repeated input validation.- The functional approach (
map,where,reduce,fold,expand) is more expressive for collection transformation and aggregation. It can be chained and needs no temporary variables.await forfor iterating aStreamasynchronously — waits for each event to finish processing before receiving the next one.- Don’t modify a collection while iterating — use
whereto build a new collection, or iterate from the back if in-place modification is required.- Compute expensive conditions once before the loop starts — don’t call slow functions in a loop condition expression that’s evaluated every iteration.
- Labels (
outerLoop:) letbreak/continuetarget an outer loop, but use them sparingly — factoring into a function withreturnis usually cleaner.