List #
List adalah koleksi berurutan yang bisa diakses via indeks — struktur data paling sering digunakan dalam hampir setiap program Dart. Di balik kesederhanaannya, List Dart menyimpan beberapa keputusan desain penting yang perlu dipahami: perbedaan antara growable dan fixed-length list, kapan List sebaiknya diganti dengan Iterable yang lebih hemat memori, bagaimana method fungsional seperti map, where, dan fold bekerja secara lazy, dan kenapa menggunakan + untuk menggabungkan list dalam loop bisa menjadi masalah performa serius. Artikel ini membahas semua ini — dari dasar hingga pola yang digunakan dalam aplikasi produksi nyata.
Membuat List #
Ada beberapa cara membuat List di Dart, masing-masing dengan karakteristik berbeda:
// 1. Literal — cara paling umum
List<int> angka = [1, 2, 3, 4, 5];
List<String> kota = ['Jakarta', 'Bandung', 'Surabaya'];
List<dynamic> campuran = [1, 'dua', true, null]; // hindari ini
// 2. List.empty() — list kosong
List<String> kosong = []; // growable (default)
List<String> kosongGrowable = List.empty(growable: true);
// 3. List.filled() — isi semua elemen dengan nilai yang sama
List<int> nol = List.filled(5, 0); // [0, 0, 0, 0, 0] — fixed-length
List<bool> flags = List.filled(3, false); // [false, false, false]
// 4. List.generate() — isi dengan fungsi generator
List<int> kuadrat = List.generate(5, (i) => i * i);
// [0, 1, 4, 9, 16]
List<String> label = List.generate(5, (i) => 'Item ${i + 1}');
// ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']
// 5. List.from() — buat dari Iterable lain
List<int> dariSet = List.from({3, 1, 4, 1, 5}); // Set ke List
List<int> dariRange = List.from(Iterable.generate(5)); // [0, 1, 2, 3, 4]
// 6. List.of() — seperti List.from() tapi mempertahankan tipe generik
List<num> nums = [1, 2.5, 3];
List<num> salin = List.of(nums); // ✓ tipe dipertahankan
Fixed-Length vs Growable #
// Growable — bisa ditambah/dikurangi elemennya (default)
List<int> growable = [1, 2, 3];
growable.add(4); // ✓
growable.remove(2); // ✓
// Fixed-length — ukuran tetap setelah dibuat
List<int> fixed = List.filled(3, 0); // [0, 0, 0]
fixed[0] = 10; // ✓ nilai bisa diubah
fixed.add(4); // ✗ UnsupportedError: tidak bisa tambah elemen
// Const — benar-benar immutable: ukuran DAN nilai tidak bisa diubah
const List<int> konstanta = [1, 2, 3];
konstanta.add(4); // ✗ UnsupportedError
konstanta[0] = 99; // ✗ UnsupportedError
Akses dan Properti Dasar #
List<String> buah = ['apel', 'jeruk', 'mangga', 'pisang', 'anggur'];
// Akses via indeks
print(buah[0]); // 'apel' — indeks pertama
print(buah[buah.length - 1]); // 'anggur' — indeks terakhir
// Properti
print(buah.first); // 'apel'
print(buah.last); // 'anggur'
print(buah.length); // 5
print(buah.isEmpty); // false
print(buah.isNotEmpty); // true
// Pencarian
print(buah.contains('mangga')); // true
print(buah.indexOf('jeruk')); // 1
print(buah.lastIndexOf('apel')); // 0
print(buah.indexWhere((b) => b.startsWith('a'))); // 0
// Akses aman — hindari RangeError
String? pertama = buah.firstOrNull; // 'apel'
String? tidakAda = buah.firstWhereOrNull((b) => b == 'durian'); // null
// ANTI-PATTERN: akses indeks tanpa pengecekan
List<String> hasil = ambilData();
print(hasil[0]); // ✗ RangeError jika list kosong
// BENAR: periksa terlebih dahulu atau gunakan firstOrNull
if (hasil.isNotEmpty) {
print(hasil.first); // ✓
}
// atau
print(hasil.firstOrNull ?? 'kosong'); // ✓ null-safe
Modifikasi List #
Menambah Elemen #
List<int> angka = [1, 2, 3];
angka.add(4); // tambah satu di akhir: [1, 2, 3, 4]
angka.addAll([5, 6, 7]); // tambah banyak di akhir: [1, 2, 3, 4, 5, 6, 7]
angka.insert(0, 0); // sisipkan di indeks 0: [0, 1, 2, 3, 4, 5, 6, 7]
angka.insertAll(1, [-2, -1]); // sisipkan beberapa: [0, -2, -1, 1, 2, ...]
// Spread operator — cara idiomatis menggabungkan
List<int> a = [1, 2, 3];
List<int> b = [4, 5, 6];
List<int> gabung = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
List<int> disisipkan = [...a, 99, ...b]; // [1, 2, 3, 99, 4, 5, 6]
// Null-aware spread
List<int>? opsional;
List<int> aman = [...a, ...?opsional, ...b]; // diabaikan jika null
Menghapus Elemen #
List<String> kota = ['Jakarta', 'Bandung', 'Surabaya', 'Bandung', 'Medan'];
kota.remove('Bandung'); // hapus kemunculan PERTAMA: ['Jakarta', 'Surabaya', 'Bandung', 'Medan']
kota.removeAt(0); // hapus di indeks 0: ['Surabaya', 'Bandung', 'Medan']
kota.removeLast(); // hapus terakhir: ['Surabaya', 'Bandung']
kota.removeWhere((k) => k.length > 7); // hapus semua yang panjangnya > 7
kota.retainWhere((k) => k.startsWith('S')); // pertahankan yang dimulai 'S'
kota.clear(); // hapus semua elemen
Mengubah Elemen #
List<int> angka = [1, 2, 3, 4, 5];
angka[2] = 99; // ubah elemen di indeks 2
angka.setAll(1, [20, 30]); // ubah beberapa mulai indeks 1: [1, 20, 30, 4, 5]
angka.fillRange(0, 3, 0); // isi rentang [0, 3) dengan 0: [0, 0, 0, 4, 5]
angka.replaceRange(1, 3, [10, 20, 30]); // ganti rentang [1, 3) dengan list baru
Method Fungsional — Kekuatan Sebenarnya List #
Method fungsional bekerja secara lazy pada Iterable — hasilnya tidak dihitung sampai diiterasi. Panggil .toList() di akhir untuk mendapat List yang terevaluasi penuh.
map — Transformasi Setiap Elemen
#
List<int> angka = [1, 2, 3, 4, 5];
// map mengembalikan Iterable<T> — lazy
Iterable<int> kuadrat = angka.map((n) => n * n);
// toList() untuk mendapat List<T>
List<int> kuadratList = angka.map((n) => n * n).toList();
// [1, 4, 9, 16, 25]
// Transformasi ke tipe berbeda
List<String> diformat = angka.map((n) => 'Nilai: $n').toList();
// ['Nilai: 1', 'Nilai: 2', ...]
// Transformasi objek
List<Produk> produk = rawData.map(Produk.dariJson).toList();
where — Filter Elemen
#
List<int> angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
List<int> genap = angka.where((n) => n.isEven).toList();
// [2, 4, 6, 8, 10]
List<int> besarGenap = angka
.where((n) => n.isEven)
.where((n) => n > 5)
.toList();
// [6, 8, 10]
reduce dan fold — Agregasi
#
List<int> angka = [1, 2, 3, 4, 5];
// reduce — 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 — aman untuk list kosong, bisa mengembalikan tipe berbeda
int jumlahFold = angka.fold(0, (acc, n) => acc + n); // 15
String digabung = angka.fold('', (acc, n) => '$acc$n'); // '12345'
// Hitung rata-rata
double rataRata = angka.fold<double>(0, (acc, n) => acc + n) / angka.length;
// 3.0
Chaining — Kekuatan Method Fungsional #
Method fungsional bisa dirantai untuk transformasi yang kompleks tanpa variabel sementara:
List<Transaksi> transaksi = ambilSemuaTransaksi();
// Satu pipeline ekspresif
double totalPemasukanBulanIni = transaksi
.where((t) => t.jenis == JenisTransaksi.pemasukan)
.where((t) => t.tanggal.month == DateTime.now().month)
.map((t) => t.jumlah)
.fold(0.0, (acc, jumlah) => acc + jumlah);
// Lebih ekspresif dari loop imperatif:
// double total = 0;
// for (final t in transaksi) {
// if (t.jenis == JenisTransaksi.pemasukan &&
// t.tanggal.month == DateTime.now().month) {
// total += t.jumlah;
// }
// }
Method Berguna Lainnya #
List<int> angka = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
// Pengecekan kondisi
print(angka.any((n) => n > 8)); // true — ada yang > 8
print(angka.every((n) => n > 0)); // true — semua > 0
print(angka.contains(4)); // true
// Pencarian
print(angka.firstWhere((n) => n > 4)); // 5 — elemen pertama > 4
print(angka.lastWhere((n) => n < 4)); // 3 — elemen terakhir < 4
print(angka.firstWhereOrNull((n) => n > 100)); // null — tidak crash
// Ekstraksi subset
print(angka.take(3).toList()); // [3, 1, 4] — 3 elemen pertama
print(angka.skip(7).toList()); // [6, 5, 3] — skip 7 elemen pertama
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, 5, 3] — skip selama < 5
print(angka.sublist(2, 5)); // [4, 1, 5] — irisan [2, 5)
// Informasi
print(angka.elementAt(3)); // 1 — sama dengan angka[3]
Sorting — Pengurutan yang Tepat #
Pengurutan Dasar #
List<int> angka = [5, 3, 1, 4, 2];
// sort() mengubah list asli (in-place)
angka.sort();
print(angka); // [1, 2, 3, 4, 5]
// Descending
angka.sort((a, b) => b.compareTo(a));
print(angka); // [5, 4, 3, 2, 1]
Sorting Objek #
class Produk {
final String nama;
final double harga;
final int stok;
const Produk({required this.nama, required this.harga, required this.stok});
}
List<Produk> produk = [...];
// Sort berdasarkan satu kriteria
produk.sort((a, b) => a.harga.compareTo(b.harga)); // harga ascending
produk.sort((a, b) => b.stok.compareTo(a.stok)); // stok descending
produk.sort((a, b) => a.nama.compareTo(b.nama)); // nama A-Z
// Sort berdasarkan beberapa kriteria (sort majemuk)
produk.sort((a, b) {
final byHarga = a.harga.compareTo(b.harga);
if (byHarga != 0) return byHarga; // utama: harga ascending
return b.stok.compareTo(a.stok); // tiebreaker: stok descending
});
// ANTI-PATTERN: sort yang mengubah list yang seharusnya immutable
final daftar = List.unmodifiable([3, 1, 2]);
daftar.sort(); // ✗ UnsupportedError — list tidak bisa dimodifikasi
// BENAR: buat salinan yang bisa dimodifikasi
final terurut = [...daftar]..sort(); // ✓ sort pada salinan
// atau
final terurut = List.of(daftar)..sort(); // ✓ List.of membuat salinan growable
Membuat List Baru yang Terurut (Non-Mutating) #
List<int> original = [3, 1, 4, 1, 5];
// Cara 1: spread + sort dengan cascade
List<int> terurut = [...original]..sort();
print(original); // [3, 1, 4, 1, 5] — tidak berubah
print(terurut); // [1, 1, 3, 4, 5]
// Cara 2: sorted dari package collection
import 'package:collection/collection.dart';
List<int> terurut2 = original.sorted(); // ✓ tidak mutate original
Collection If dan Collection For #
Dart mendukung if dan for di dalam literal koleksi — cara idiomatis membangun list secara kondisional:
bool tampilkanBonus = true;
List<String> menu = [
'Nasi Goreng',
'Mie Goreng',
if (tampilkanBonus) 'Es Krim Gratis', // hanya ditambahkan jika true
if (DateTime.now().weekday == DateTime.friday) 'Promo Jumat',
];
// Collection for
List<int> angka = [1, 2, 3];
List<Widget> cards = [
for (final n in angka) ...[
TitleCard(n),
if (n % 2 == 0) EvenBadge(), // kondisi di dalam collection for
],
];
// Collection if-else
String level = 'premium';
List<String> fitur = [
'Fitur Dasar',
if (level == 'premium') ...[
'Fitur Premium A',
'Fitur Premium B',
] else [
'Upgrade ke Premium',
],
];
Lazy Iterable vs Eager List #
Ini adalah salah satu perbedaan yang paling berdampak pada performa dan jarang dipahami:
// EAGER (List) — seluruh hasil dihitung dan disimpan di memori sekaligus
List<int> angka = List.generate(1_000_000, (i) => i);
List<int> hasilEager = angka
.map((n) => n * n) // buat List 1 juta elemen
.where((n) => n > 10) // buat List baru lagi dari 1 juta elemen
.toList(); // akhirnya toList()
// Memori: alokasi 3 kali untuk list besar
// LAZY (Iterable) — hanya dihitung saat dibutuhkan
Iterable<int> hasilLazy = angka
.map((n) => n * n) // belum dihitung — hanya "resep"
.where((n) => n > 10); // belum dihitung — hanya "resep" tambahan
// Dihitung satu per satu saat diiterasi
for (final n in hasilLazy) {
// setiap n baru dihitung di sini — tidak ada alokasi list besar di memori
if (n > 1000) break; // bisa berhenti di tengah — hemat lebih banyak
}
// Kapan pakai Iterable (lazy) vs List (eager)
// GUNAKAN Iterable ketika:
// - Koleksi sangat besar atau tidak terbatas
// - Kemungkinan berhenti iterasi sebelum selesai (take, firstWhere)
// - Hanya butuh iterasi sekali
// - Chaining banyak transformasi
// GUNAKAN List ketika:
// - Perlu akses random (via indeks)
// - Perlu diiterasi lebih dari sekali
// - Perlu length yang akurat sebelum iterasi
// - Menyerahkan data ke API yang menerima List
List yang Tidak Bisa Dimodifikasi #
Untuk API publik atau data yang tidak boleh diubah setelah dihasilkan, bungkus list dengan List.unmodifiable atau UnmodifiableListView:
class KatalogProduk {
final List<Produk> _produk;
KatalogProduk(List<Produk> produk) : _produk = List.of(produk);
// Kembalikan view yang tidak bisa dimodifikasi
List<Produk> get produk => List.unmodifiable(_produk);
// Atau gunakan UnmodifiableListView dari dart:collection — lebih hemat memori
// (tidak membuat salinan, hanya wrapper)
List<Produk> get produkView => UnmodifiableListView(_produk);
}
// Pemanggil tidak bisa mengubah list internal
final katalog = KatalogProduk([p1, p2, p3]);
katalog.produk.add(p4); // ✗ UnsupportedError
katalog.produk[0] = p4; // ✗ UnsupportedError
// ANTI-PATTERN: mengekspos list internal langsung
class ProdukRepository {
final List<Produk> _data = [];
List<Produk> get semua => _data; // ✗ pemanggil bisa modifikasi _data langsung!
}
// BENAR: kembalikan view atau salinan yang tidak bisa dimodifikasi
class ProdukRepository {
final List<Produk> _data = [];
List<Produk> get semua => List.unmodifiable(_data); // ✓
// atau: UnmodifiableListView(_data) untuk zero-copy
}
Menggabungkan dan Meratakan List #
// Menggabungkan beberapa list — cara yang benar
List<int> a = [1, 2, 3];
List<int> b = [4, 5, 6];
List<int> c = [7, 8, 9];
// Spread — paling idiomatis untuk jumlah list yang diketahui
List<int> gabung = [...a, ...b, ...c]; // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// expand — untuk meratakan 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]
// Menggabungkan banyak list secara dinamis
List<List<int>> semuaList = [a, b, c];
List<int> gabungDinamis = semuaList.expand((l) => l).toList();
// ANTI-PATTERN: menggunakan += atau + dalam loop — O(n²) memory
List<int> hasil = [];
for (final subList in semuaList) {
hasil = hasil + subList; // ✗ membuat List baru setiap iterasi
}
// ANTI-PATTERN: addAll dalam loop — masih O(n) tapi verbose
List<int> hasil = [];
for (final subList in semuaList) {
hasil.addAll(subList); // bisa, tapi expand lebih idiomatis
}
// BENAR: expand atau spread
List<int> hasil = semuaList.expand((l) => l).toList(); // ✓ O(n), paling idiomatis
Grouping dan Partitioning #
Untuk operasi grouping yang sering dibutuhkan di aplikasi nyata, gunakan package collection:
import 'package:collection/collection.dart';
List<Transaksi> transaksi = [...];
// groupBy — kelompokkan berdasarkan kriteria
Map<String, List<Transaksi>> perKategori =
groupBy(transaksi, (t) => t.kategori);
// {'makanan': [...], 'transport': [...], 'belanja': [...]}
// partition — bagi list menjadi dua berdasarkan kondisi
final (berhasil, gagal) = transaksi.partition((t) => t.sukses);
// berhasil: semua yang sukses, gagal: semua yang tidak sukses
// Jika tidak ingin tambahkan package, implementasi manual:
Map<K, List<T>> groupBy<T, K>(List<T> list, K Function(T) keyFn) {
final map = <K, List<T>>{};
for (final item in list) {
map.putIfAbsent(keyFn(item), () => []).add(item);
}
return map;
}
Anti-Pattern List yang Harus Dihindari #
Modifikasi List Saat Iterasi #
List<int> angka = [1, 2, 3, 4, 5];
// ANTI-PATTERN: modifikasi selama for-in — ConcurrentModificationError
for (final n in angka) {
if (n.isEven) angka.remove(n); // ✗ error runtime
}
// BENAR: filter ke list baru (paling idiomatis)
angka = angka.where((n) => n.isOdd).toList(); // ✓
// BENAR: iterasi mundur untuk modifikasi in-place
for (int i = angka.length - 1; i >= 0; i--) {
if (angka[i].isEven) angka.removeAt(i); // ✓ aman karena iterasi dari belakang
}
Pengecekan Duplikat dengan contains dalam Loop
#
// ANTI-PATTERN: List.contains dalam loop — O(n²)
List<String> unik = [];
for (final item in daftar) {
if (!unik.contains(item)) { // ✗ O(n) per iterasi — total O(n²)
unik.add(item);
}
}
// BENAR: gunakan Set untuk deduplication — O(n)
List<String> unik = daftar.toSet().toList(); // ✓ O(n)
// Catatan: urutan mungkin tidak terjaga — gunakan LinkedHashSet jika perlu
Concatenation String dalam Loop #
// ANTI-PATTERN: konkatenasi String dengan join di list besar
List<String> kata = List.generate(10000, (i) => 'kata$i');
String hasil = '';
for (final k in kata) {
hasil += '$k '; // ✗ membuat String baru setiap iterasi — O(n²) memori
}
// BENAR: gunakan join (O(n))
String hasil = kata.join(' '); // ✓ paling efisien
// Atau StringBuffer jika perlu kontrol lebih
final buffer = StringBuffer();
for (final k in kata) {
buffer.write(k);
buffer.write(' ');
}
String hasil = buffer.toString(); // ✓ O(n)
Ringkasan #
- Tiga cara membuat List yang paling berguna: literal
[...],List.generate()untuk list berpola, danList.from()/List.of()untuk mengonversi dari Iterable.- Fixed-length vs growable:
List.filled()menghasilkan fixed-length — ukuran tidak bisa diubah. Literal[]danList.empty(growable: true)menghasilkan growable.const Listsepenuhnya immutable: ukuran dan nilai tidak bisa diubah. Berbeda darifinal Listyang hanya referensinya yang tidak bisa diganti tapi isinya masih bisa.- Method fungsional bekerja lazy (
map,where,take,skip) — hasilnya berupaIterableyang belum dievaluasi. Panggil.toList()untuk mendapatListyang terevaluasi.- Chaining method membuat pipeline transformasi yang ekspresif tanpa variabel sementara — lebih terbaca dari loop imperatif untuk operasi filter, transform, dan agregasi.
reducethrow jika list kosong — gunakanfolddengan nilai awal untuk keamanan, terutama saat bekerja dengan data yang bisa kosong.- Collection if dan collection for memungkinkan membangun list secara kondisional langsung di dalam literal — idiom Dart yang sangat ekspresif.
- Spread
...dan...?adalah cara terbaik menggabungkan list yang jumlahnya diketahui. Untuk menggabungkan list secara dinamis, gunakanexpand.- Jangan ekspos list internal langsung — kembalikan
List.unmodifiable()atauUnmodifiableListViewagar pemanggil tidak bisa merusak state internal.- Gunakan
Setuntuk deduplication, bukanList.containsdalam loop.containspada List adalah O(n), menjadikan loop deduplikasi O(n²) — sangat lambat untuk data besar.