Perulangan

Perulangan #

Perulangan adalah mekanisme untuk mengeksekusi blok kode lebih dari sekali. Dart menyediakan empat konstruksi imperatif (for, for-in, while, do-while) dan satu set method fungsional (map, where, reduce, fold) yang masing-masing memiliki karakter berbeda. Memilih yang tepat bukan hanya soal “semua bisa melakukan hal yang sama” — setiap konstruksi mengkomunikasikan niat yang berbeda kepada pembaca kode. for klasik mengatakan “saya butuh indeks”, for-in mengatakan “saya hanya butuh elemennya”, dan map mengatakan “saya mentransformasi koleksi ini”. Artikel ini membahas kapan menggunakan masing-masing, anti-pattern yang harus dihindari, dan topik yang jarang dibahas seperti perulangan asinkron.

for Klasik #

for dengan tiga komponen (inisialisasi; kondisi; update) adalah bentuk perulangan paling eksplisit. Gunakan ketika kamu membutuhkan akses ke indeks, atau ketika logika increment-nya tidak standar.

// Struktur dasar
for (int i = 0; i < 5; i++) {
  print('Iterasi $i');
}
// Output: Iterasi 0, Iterasi 1, ... Iterasi 4

// Iterasi mundur
for (int i = 10; i >= 0; i -= 2) {
  print(i); // 10, 8, 6, 4, 2, 0
}

// Inisialisasi di luar — variabel tetap bisa diakses setelah loop
int i = 0;
for (; i < 5; i++) {
  if (i == 3) break;
}
print(i); // 3 — masih bisa diakses

Akses Indeks Bersamaan dengan Elemen #

Salah satu alasan utama memilih for klasik adalah kebutuhan akan indeks:

List<String> produk = ['Laptop', 'Mouse', 'Keyboard', 'Monitor'];

// Gunakan for klasik ketika butuh indeks
for (int i = 0; i < produk.length; i++) {
  print('${i + 1}. ${produk[i]}'); // "1. Laptop", "2. Mouse", dst
}

// Alternatif idiomatis: asMap() + for-in
for (final entry in produk.asMap().entries) {
  print('${entry.key + 1}. ${entry.value}');
}

// Atau dengan indexed() dari Dart 3.x
for (final (i, item) in produk.indexed) {
  print('${i + 1}. $item');
}
// ANTI-PATTERN: menggunakan for klasik hanya untuk mengakses elemen
for (int i = 0; i < produk.length; i++) {
  print(produk[i]); // i tidak digunakan selain untuk akses — mubazir
}

// BENAR: gunakan for-in jika tidak butuh indeks
for (final p in produk) {
  print(p); // lebih bersih dan niatnya jelas
}

for-in — Iterasi Koleksi #

for-in adalah bentuk perulangan yang paling idiomatis di Dart untuk mengiterasi elemen koleksi. Ia bekerja dengan semua objek yang mengimplementasikan Iterable — termasuk List, Set, Map.entries, Map.keys, Map.values, dan String (iterasi per karakter).

// 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 — iterasi entries
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
for (final entry in skor.entries) {
  print('${entry.key}: ${entry.value}');
}

// Iterasi hanya key atau hanya value
for (final nama in skor.keys) print(nama);
for (final nilai in skor.values) print(nilai);

// String — iterasi per karakter
for (final karakter in 'Dart') {
  print(karakter); // D, a, r, t
}

for-in dengan Iterable Generator #

for-in juga bekerja dengan lazy iterable — koleksi yang dievaluasi satu per satu saat diiterasi, bukan seluruhnya dimuat ke memori:

// Range-like iteration dengan 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
}

// Atau gunakan where/map pada iterable yang ada
for (final n in List.generate(100, (i) => i).where((n) => n.isEven).take(5)) {
  print(n); // 0, 2, 4, 6, 8 — lazy, hanya mengambil 5 elemen genap pertama
}

forEach — Method Iterasi #

forEach adalah method pada Iterable yang menerima fungsi callback dan memanggilnya untuk setiap elemen. Secara fungsional mirip dengan for-in, tapi memiliki keterbatasan penting.

List<int> angka = [1, 2, 3, 4, 5];

// forEach dengan lambda
angka.forEach((n) => print(n * 2));

// forEach dengan referensi fungsi
angka.forEach(print); // sama dengan (n) => print(n)

// Map.forEach — menerima key dan value
Map<String, int> skor = {'Budi': 90, 'Siti': 85};
skor.forEach((nama, nilai) => print('$nama: $nilai'));

Keterbatasan forEach #

// ANTI-PATTERN: menggunakan forEach ketika butuh break atau continue
List<int> angka = [1, 2, 3, 4, 5];
angka.forEach((n) {
  if (n == 3) break;    // ✗ error: tidak bisa break di dalam forEach
  if (n == 2) continue; // ✗ error: tidak bisa continue di dalam forEach
  print(n);
});

// BENAR: gunakan for-in jika butuh break atau continue
for (final n in angka) {
  if (n == 3) break;    // ✓
  if (n == 2) continue; // ✓
  print(n);
}
// ANTI-PATTERN: forEach dengan async/await — tidak menunggu dengan benar
List<String> ids = ['U001', 'U002', 'U003'];
ids.forEach((id) async {
  await hapusPengguna(id); // ✗ forEach tidak await Future — semua berjalan paralel
});
// Program mungkin sudah selesai sebelum semua penghapusan benar-benar tuntas

// BENAR: gunakan for-in dengan async/await
for (final id in ids) {
  await hapusPengguna(id); // ✓ menunggu satu per satu
}

// Atau jika memang mau paralel, gunakan Future.wait secara eksplisit
await Future.wait(ids.map((id) => hapusPengguna(id)));

while — Perulangan Berbasis Kondisi #

while mengevaluasi kondisi sebelum setiap iterasi. Gunakan ketika jumlah iterasi tidak diketahui di awal dan kondisi berhentinya bergantung pada sesuatu yang berubah di dalam loop.

// Membaca sampai kondisi terpenuhi
int nilaiInput = 0;
while (nilaiInput <= 0) {
  nilaiInput = bacaInput(); // loop terus sampai input positif
}

// Traversal struktur data hierarkis
TreeNode? node = pohon.akar;
while (node != null && !node.adalahTarget) {
  node = node.kiri ?? node.kanan;
}

// Polling dengan batas waktu
final batas = DateTime.now().add(Duration(seconds: 30));
while (DateTime.now().isBefore(batas)) {
  final hasil = cekStatus();
  if (hasil == 'selesai') break;
  sleep(Duration(seconds: 1));
}

Waspada Infinite Loop #

while adalah satu-satunya konstruksi perulangan yang memungkinkan infinite loop secara tidak sengaja jika kondisi berhentinya tidak pernah terpenuhi:

// ANTI-PATTERN: kondisi berhenti yang tidak pernah tercapai
int n = 1;
while (n > 0) {
  n++;   // n selalu > 0 karena terus bertambah — infinite loop!
  print(n);
}

// ANTI-PATTERN: lupa mengupdate variabel kondisi
int hitung = 0;
while (hitung < 10) {
  print(hitung); // ✗ hitung tidak pernah bertambah — infinite loop!
}

// BENAR: pastikan kondisi berhenti pasti tercapai
int hitung = 0;
while (hitung < 10) {
  print(hitung);
  hitung++; // ✓ kondisi akhirnya akan false
}

do-while — Eksekusi Minimal Sekali #

do-while mengevaluasi kondisi setelah setiap iterasi, menjamin body loop dieksekusi minimal sekali — bahkan jika kondisinya sudah false sejak awal.

// Skenario klasik: menu yang ditampilkan minimal sekali
String pilihan;
do {
  tampilkanMenu();
  pilihan = bacaInput();
} while (pilihan != 'keluar');

// Validasi input interaktif
int nilai;
do {
  print('Masukkan nilai antara 1-100:');
  nilai = int.parse(bacaInput());
} while (nilai < 1 || nilai > 100);
print('Nilai valid: $nilai');
// ANTI-PATTERN: menggunakan do-while padahal kondisi mungkin langsung false
// dan eksekusi pertama tidak diharapkan
do {
  kirimNotifikasi(pengguna); // ✗ dikirim meski pengguna tidak aktif
} while (pengguna.aktif);

// BENAR: cek kondisi dulu dengan while jika eksekusi pertama tidak dijamin
if (pengguna.aktif) {
  do {
    kirimNotifikasi(pengguna);
  } while (pengguna.aktif && pengguna.butuhPengingat());
}
// Atau lebih sederhana: gunakan while biasa
while (pengguna.aktif) {
  kirimNotifikasi(pengguna);
}

Perbandingan Konstruksi Perulangan #

flowchart TD
    A{Apa yang diiterasi?} --> B{Koleksi / Iterable?}
    B -- Ya --> C{Butuh indeks\natau break/continue?}
    C -- Hanya elemen,\ntidak butuh break --> D[for-in atau\nmetode fungsional]
    C -- Butuh indeks --> E[for klasik\natau asMap\natau indexed]
    C -- Butuh break/continue --> F[for-in]
    B -- Tidak\nloop kondisional --> G{Body harus\njalan minimal sekali?}
    G -- Ya --> H[do-while]
    G -- Tidak --> I[while]
Konstruksi Cocok untuk Bisa break/continue Bisa async/await
for klasik Iterasi dengan indeks, increment custom
for-in Iterasi elemen koleksi
forEach Iterasi sederhana tanpa break ✗ (tidak berfungsi)
while Kondisi berhenti tidak diketahui di awal
do-while Body harus jalan minimal sekali

break dan continue #

break menghentikan loop sepenuhnya; continue melewati sisa iterasi saat ini dan langsung ke iterasi berikutnya. Keduanya bekerja di semua konstruksi imperatif (for, for-in, while, do-while).

List<int> angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// break — berhenti saat menemukan elemen pertama > 5
for (final n in angka) {
  if (n > 5) break;
  print(n); // 1, 2, 3, 4, 5
}

// continue — lewati bilangan ganjil
for (final n in angka) {
  if (n.isOdd) continue;
  print(n); // 2, 4, 6, 8, 10
}

// break di while — keluar saat kondisi kompleks terpenuhi
int i = 0;
while (true) { // infinite loop yang dikendalikan break
  i++;
  if (i * i > 50) break;
}
print(i); // 8 — karena 8² = 64 > 50

Label untuk Loop Bersarang #

Label memungkinkan break dan continue menarget loop luar dari dalam loop dalam — sangat berguna pada nested loop:

// Tanpa label — break hanya keluar dari loop dalam
for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (j == 1) break; // hanya keluar dari loop j
    print('$i,$j');
  }
}
// Output: 0,0 | 1,0 | 2,0

// Dengan label — break keluar dari loop luar
luarLoop:
for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (i == 1 && j == 1) break luarLoop; // keluar dari keduanya
    print('$i,$j');
  }
}
// Output: 0,0 | 0,1 | 0,2 | 1,0

// continue dengan label — skip ke iterasi berikutnya di loop luar
luarLoop:
for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (j == 1) continue luarLoop; // skip sisa loop dalam, lanjut i berikutnya
    print('$i,$j');
  }
}
// Output: 0,0 | 1,0 | 2,0
Label membuat alur kontrol menjadi tidak linear dan sulit dilacak. Sebelum menggunakan label, pertimbangkan apakah logika bisa difaktorisasi ke dalam fungsi terpisah dengan return — yang jauh lebih mudah dibaca.

Pendekatan Fungsional: map, where, reduce, fold #

Dart mendukung gaya pemrograman fungsional untuk transformasi koleksi. Pendekatan fungsional sering lebih ekspresif dan ringkas dibanding loop imperatif — terutama untuk operasi umum seperti filter, transformasi, dan agregasi.

map — Transformasi Setiap Elemen #

List<int> angka = [1, 2, 3, 4, 5];

// Imperatif — verbose
List<int> hasilImperatif = [];
for (final n in angka) {
  hasilImperatif.add(n * n);
}

// Fungsional — ringkas dan ekspresif
List<int> hasilFungsional = angka.map((n) => n * n).toList();
// [1, 4, 9, 16, 25]

// map bisa dirantai
List<String> diformat = angka
    .map((n) => n * n)          // kuadratkan
    .where((n) => n > 5)        // filter > 5
    .map((n) => 'nilai: $n')    // format ke String
    .toList();
// ['nilai: 9', 'nilai: 16', 'nilai: 25']

where — Filter Elemen #

List<Produk> produk = ambilSemuaProduk();

// Imperatif
List<Produk> tersedia = [];
for (final p in produk) {
  if (p.stok > 0 && p.aktif) {
    tersedia.add(p);
  }
}

// Fungsional
List<Produk> tersedia = produk
    .where((p) => p.stok > 0 && p.aktif)
    .toList();

reduce dan fold — Agregasi #

List<int> angka = [1, 2, 3, 4, 5];

// reduce — agregasi tanpa nilai awal (throws jika list kosong)
int jumlah = angka.reduce((acc, n) => acc + n);    // 15
int maks = angka.reduce((a, b) => a > b ? a : b);  // 5

// fold — agregasi dengan nilai awal (aman untuk list kosong)
int jumlahFold = angka.fold(0, (acc, n) => acc + n);      // 15
double rataRata = angka.fold(0, (acc, n) => acc + n) / angka.length; // 3.0

// fold untuk agregasi ke tipe berbeda
String digabung = angka.fold('', (acc, n) => '$acc$n'); // '12345'

// fold untuk membangun Map dari List
Map<String, int> panjangKata = ['halo', 'dart', 'pemrograman'].fold(
  {},
  (acc, kata) => acc..addAll({kata: kata.length}),
);
// {'halo': 4, 'dart': 4, 'pemrograman': 11}

Method Agregasi Lain yang Berguna #

List<int> angka = [3, 1, 4, 1, 5, 9, 2, 6];

print(angka.any((n) => n > 8));          // true — ada yang > 8
print(angka.every((n) => n > 0));        // true — semua > 0
print(angka.contains(5));               // true
print(angka.indexOf(4));                // 2 — indeks pertama nilai 4
print(angka.firstWhere((n) => n > 4));  // 5 — elemen pertama > 4
print(angka.lastWhere((n) => n < 4));   // 2 — elemen terakhir < 4
print(angka.takeWhile((n) => n < 5).toList()); // [3, 1, 4, 1] — ambil selama < 5
print(angka.skipWhile((n) => n < 5).toList()); // [5, 9, 2, 6] — skip selama < 5

// Menghindari firstWhere yang throw jika tidak ada
int? hasil = angka.firstWhereOrNull((n) => n > 100); // null, tidak throw

Perulangan Asinkron: await for #

Dart mendukung perulangan asinkron untuk Stream — urutan data yang dikirimkan seiring waktu. await for menunggu setiap event dari stream sebelum melanjutkan ke iterasi berikutnya.

// Stream dari generator async
Stream<int> hitungMundur(int dari) async* {
  for (int i = dari; i >= 0; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

// Mengiterasi stream dengan await for
Future<void> main() async {
  await for (final angka in hitungMundur(5)) {
    print(angka); // 5, 4, 3, 2, 1, 0 — satu per detik
  }
  print('Selesai!');
}

await for vs listen #

// listen — non-blocking, tidak menunggu setiap event selesai diproses
stream.listen((event) {
  prosesEvent(event); // bisa berjalan overlap jika prosesEvent lambat
});

// await for — blocking, menunggu setiap iterasi selesai
await for (final event in stream) {
  await prosesEvent(event); // menunggu selesai sebelum event berikutnya
}
// ANTI-PATTERN: menggunakan forEach pada Stream
stream.forEach((event) async {
  await prosesEvent(event); // ✗ tidak menunggu dengan benar
});

// BENAR: gunakan await for untuk iterasi Stream dengan async
await for (final event in stream) {
  await prosesEvent(event); // ✓ menunggu setiap event
}

Perulangan Bersarang dan Kompleksitasnya #

Nested loop yang dalam adalah salah satu penyebab kode lambat dan sulit dibaca. Setiap level nesting menambah satu dimensi kompleksitas — dua loop berarti O(n²), tiga loop berarti O(n³).

// O(n²) — dapat diterima untuk n kecil, perhatikan untuk n besar
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: triple nested loop yang bisa dioptimalkan
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);
      }
    }
  }
}

// BENAR: gunakan pendekatan fungsional yang lebih ekspresif
List<Produk> produkDitemukan = toko
    .expand((t) => t.kategori)
    .expand((k) => k.produk)
    .where((p) => p.harga < 50000)
    .toList();

expand adalah method yang “meratakan” satu level koleksi — mengubah List<List<T>> menjadi 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]

// Ekuivalen dengan flatMap di bahasa lain

Kapan Memilih Imperatif vs Fungsional #

Pertanyaan yang sering muncul: kapan harus menggunakan for-in vs map/where/reduce? Jawabannya bergantung pada tujuan operasi:

GUNAKAN pendekatan FUNGSIONAL (map, where, reduce, fold) ketika:
  ✓ Mentransformasi koleksi menjadi koleksi baru
  ✓ Memfilter elemen berdasarkan kondisi
  ✓ Mengagregasi koleksi menjadi satu nilai
  ✓ Operasi bisa dirantai tanpa variabel sementara
  ✓ Tidak ada side effect (tidak memodifikasi state luar)

GUNAKAN pendekatan IMPERATIF (for, for-in, while) ketika:
  ✓ Butuh break atau continue untuk keluar lebih awal
  ✓ Ada side effect yang perlu dikontrol (menulis file, update UI)
  ✓ Iterasi asinkron (await for, await di dalam loop)
  ✓ Logika terlalu kompleks untuk ekspresi lambda satu baris
  ✓ Butuh akses ke beberapa variabel di luar loop secara terkoordinasi

Anti-Pattern Perulangan #

Modifikasi Koleksi Saat Diiterasi #

List<int> angka = [1, 2, 3, 4, 5];

// ANTI-PATTERN: modifikasi list selama iterasi — perilaku tidak terduga
for (final n in angka) {
  if (n.isEven) angka.remove(n); // ✗ ConcurrentModificationError saat runtime
}

// BENAR: buat list baru atau iterasi salinan
// Opsi 1: filter ke list baru
angka = angka.where((n) => n.isOdd).toList();

// Opsi 2: iterasi dari belakang jika harus modifikasi in-place
for (int i = angka.length - 1; i >= 0; i--) {
  if (angka[i].isEven) angka.removeAt(i);
}

Akumulasi dengan + dalam Loop #

// ANTI-PATTERN: konkatenasi String dengan + dalam loop — O(n²)
String hasil = '';
for (final kata in daftarKata) {
  hasil += kata + ' '; // membuat objek String baru setiap iterasi
}

// BENAR: gunakan StringBuffer — O(n)
final buffer = StringBuffer();
for (final kata in daftarKata) {
  buffer.write(kata);
  buffer.write(' ');
}
String hasil = buffer.toString();

// Atau join untuk kasus sederhana
String hasil = daftarKata.join(' ');

Kondisi Loop yang Dihitung Ulang Setiap Iterasi #

// ANTI-PATTERN: memanggil method mahal di kondisi loop setiap iterasi
for (int i = 0; i < ambilPanjangDariDatabase(); i++) { // ✗ query DB setiap iterasi
  proses(i);
}

// BENAR: hitung sekali, simpan di variabel
final panjang = ambilPanjangDariDatabase();
for (int i = 0; i < panjang; i++) { // ✓ hanya satu kali query
  proses(i);
}

// Untuk List, ini sudah efisien karena .length adalah O(1)
for (int i = 0; i < list.length; i++) { ... } // ✓ fine untuk List

Ringkasan #

  • for klasik untuk iterasi dengan indeks, increment non-standar, atau akses ke beberapa posisi sekaligus. Gunakan asMap().entries atau indexed jika butuh indeks sekaligus elemen.
  • for-in adalah cara paling idiomatis mengiterasi koleksi di Dart — gunakan selalu kecuali butuh indeks atau increment kustom.
  • forEach punya keterbatasan penting: tidak mendukung break, continue, dan async/await. Gunakan hanya untuk iterasi sederhana tanpa kontrol alur.
  • while untuk kondisi berhenti yang tidak diketahui di awal — pastikan kondisi berhenti pasti tercapai untuk menghindari infinite loop.
  • do-while untuk body yang harus dieksekusi minimal sekali — paling umum untuk menu interaktif dan validasi input berulang.
  • Pendekatan fungsional (map, where, reduce, fold, expand) lebih ekspresif untuk transformasi dan agregasi koleksi. Bisa dirantai dan tidak membutuhkan variabel sementara.
  • await for untuk mengiterasi Stream secara asinkron — menunggu setiap event selesai diproses sebelum menerima event berikutnya.
  • Jangan modifikasi koleksi saat diiterasi — gunakan where untuk membuat koleksi baru, atau iterasi dari belakang jika harus modifikasi in-place.
  • Hitung kondisi mahal sekali sebelum loop dimulai — jangan panggil fungsi lambat di ekspresi kondisi loop yang dievaluasi setiap iterasi.
  • Label (outerLoop:) memungkinkan break/continue menarget loop luar, tapi gunakan sparingly — faktoring ke fungsi dengan return biasanya lebih bersih.

← Sebelumnya: Seleksi Kondisi   Berikutnya: Fungsi →

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