List

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, dan List.from() / List.of() untuk mengonversi dari Iterable.
  • Fixed-length vs growable: List.filled() menghasilkan fixed-length — ukuran tidak bisa diubah. Literal [] dan List.empty(growable: true) menghasilkan growable.
  • const List sepenuhnya immutable: ukuran dan nilai tidak bisa diubah. Berbeda dari final List yang hanya referensinya yang tidak bisa diganti tapi isinya masih bisa.
  • Method fungsional bekerja lazy (map, where, take, skip) — hasilnya berupa Iterable yang belum dievaluasi. Panggil .toList() untuk mendapat List yang terevaluasi.
  • Chaining method membuat pipeline transformasi yang ekspresif tanpa variabel sementara — lebih terbaca dari loop imperatif untuk operasi filter, transform, dan agregasi.
  • reduce throw jika list kosong — gunakan fold dengan 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, gunakan expand.
  • Jangan ekspos list internal langsung — kembalikan List.unmodifiable() atau UnmodifiableListView agar pemanggil tidak bisa merusak state internal.
  • Gunakan Set untuk deduplication, bukan List.contains dalam loop. contains pada List adalah O(n), menjadikan loop deduplikasi O(n²) — sangat lambat untuk data besar.

← Sebelumnya: Eksepsi   Berikutnya: Map →

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