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]
ConstructBest forCan break/continueCan async/await
Classic forIndex-based iteration, custom increments
for-inIterating collection elements
forEachSimple iteration without break✗ (doesn’t work)
whileStopping condition unknown upfront
do-whileBody 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 for for index-based iteration, non-standard increments, or access to several positions at once. Use asMap().entries or indexed when you need both the index and the element.
  • for-in is the most idiomatic way to iterate collections in Dart — use it always unless you need an index or a custom increment.
  • forEach has important limitations: no break, no continue, and no proper async/await. Use it only for simple iteration without control flow.
  • while for stopping conditions unknown upfront — make sure the stopping condition is definitely reached to avoid infinite loops.
  • do-while for 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 for for iterating a Stream asynchronously — waits for each event to finish processing before receiving the next one.
  • Don’t modify a collection while iterating — use where to 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:) let break/continue target an outer loop, but use them sparingly — factoring into a function with return is usually cleaner.

← Previous: Conditional Logic   Next: Functions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact