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 #
forklasik untuk iterasi dengan indeks, increment non-standar, atau akses ke beberapa posisi sekaligus. GunakanasMap().entriesatauindexedjika butuh indeks sekaligus elemen.for-inadalah cara paling idiomatis mengiterasi koleksi di Dart — gunakan selalu kecuali butuh indeks atau increment kustom.forEachpunya keterbatasan penting: tidak mendukungbreak,continue, danasync/await. Gunakan hanya untuk iterasi sederhana tanpa kontrol alur.whileuntuk kondisi berhenti yang tidak diketahui di awal — pastikan kondisi berhenti pasti tercapai untuk menghindari infinite loop.do-whileuntuk 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 foruntuk mengiterasiStreamsecara asinkron — menunggu setiap event selesai diproses sebelum menerima event berikutnya.- Jangan modifikasi koleksi saat diiterasi — gunakan
whereuntuk 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:) memungkinkanbreak/continuemenarget loop luar, tapi gunakan sparingly — faktoring ke fungsi denganreturnbiasanya lebih bersih.