Collections #
Di dalam pengembangan aplikasi menggunakan Flutter, kita tidak hanya berurusan dengan variabel-variabel tunggal, melainkan dengan sekumpulan data yang dinamis dan kompleks — seperti daftar artikel dari server, pengaturan konfigurasi pengguna, hingga hierarki komponen visual. Kemampuan untuk mengelola, menyaring, dan mentransformasikan struktur data ini bertumpu sepenuhnya pada sistem Collections di Dart. Kita akan membedah secara mendalam jenis-jenis koleksi utama di Dart seperti List, Set, Map, Queue, serta LinkedList, menganalisis perbedaan efisiensi kinerjanya, menguasai operator modern seperti Spread Operator dan Collection If/For, serta mendalami operasi fungsional yang fleksibel dan efisien.
Panduan Memilih Koleksi yang Tepat (Matriks Keputusan) #
Setiap jenis koleksi di Dart dirancang dengan karakteristik performa dan algoritma penyimpanan yang berbeda. Memilih struktur data yang salah dapat berdampak buruk pada performa aplikasi kita, terutama saat mengelola ribuan item data pada layar yang memerlukan animasi halus.
Sebelum kita membahas kode dan instalasinya, mari kita perhatikan diagram alir keputusan di bawah ini untuk menentukan koleksi mana yang paling cocok untuk kebutuhan data kita:
flowchart TD
Start["Bagaimana karakteristik data kita?"] --> Q1{"Apakah data memiliki pasangan Key-Value?"}
Q1 -- "Ya" --> Map["Pilih: Map"]
Q1 -- "Tidak" --> Q2{"Apakah data harus unik tanpa duplikat?"}
Q2 -- "Ya" --> Set["Pilih: Set"]
Q2 -- "Tidak" --> Q3{"Apakah kita butuh operasi cepat di kedua ujung FIFO/LIFO?"}
Q3 -- "Ya" --> Queue["Pilih: Queue"]
Q3 -- "Tidak" --> List["Pilih: List"]Secara umum:
- Gunakan
Listjika urutan item sangat penting dan kita perlu mengakses elemen berdasarkan nomor indeks. - Gunakan
Setjika keunikan data adalah prioritas utama (misalnya daftar ID terpilih) dan kita ingin pengecekan keberadaan data berjalan sangat cepat. - Gunakan
Mapjika kita ingin mengasosiasikan suatu data pencari dengan nilai data lainnya.
List — Koleksi Data Berurutan dengan Indeks #
List adalah jenis koleksi yang paling umum digunakan di Dart. Koleksi ini menyimpan elemen secara berurutan (ordered collection) di mana setiap elemen dapat diakses secara langsung melalui indeks numerik berbasis nol (zero-based index).
Di Dart, List terbagi menjadi dua kategori utama berdasarkan kemampuan alokasi ukuran memorinya:
1. Growable List (Kapasitas Dinamis) #
Secara standar bawaan, saat kita membuat List literal, ukurannya bersifat dinamis. Kita dapat bebas menambah atau mengurangi elemen kapan saja. Dart secara otomatis mengalokasikan ruang memori ekstra di latar belakang saat daftar tersebut tumbuh penuh.
// Membuat growable list
final names = <String>[];
names.add('Andi');
names.add('Siti'); // Ukuran list otomatis bertambah menjadi 2
2. Fixed-Length List (Kapasitas Tetap) #
Fixed-length list didefinisikan dengan ukuran panjang yang statis sejak awal dibuat. Kita tidak diperbolehkan menambah atau menghapus elemen dari daftar ini, namun kita bisa mengubah nilai elemen yang sudah ada di dalam batas indeks tersebut.
// Membuat fixed-length list dengan 3 elemen bernilai awal null
final fixedList = List<String?>.filled(3, null, growable: false);
fixedList[0] = 'Budi'; // VALID
// JANGAN: Mencoba menambah elemen baru ke fixed-length list
// fixedList.add('Sari'); // ERROR runtime: Unsupported operation: Cannot add to a fixed-length list
Konstruktor Lanjutan List #
Dart menyediakan beberapa konstruktor pembantu untuk menginisialisasi List secara instan:
List.generate: Membuat elemen secara dinamis berdasarkan formula matematika atau indeks pembuatan.
// Membuat list angka kuadrat: [0, 1, 4, 9, 16]
final squares = List.generate(5, (index) => index * index);
List.unmodifiable: Membuat list yang sama sekali tidak dapat diubah baik nilainya maupun ukurannya (immutable list). Konstruktor ini sangat baik untuk melindungi data dari perubahan yang tidak disengaja.
final original = [1, 2, 3];
final readOnlyList = List.unmodifiable(original);
// readOnlyList[0] = 99; // ERROR runtime: Unsupported operation: Cannot modify an unmodifiable list
Set — Himpunan Data Unik Berkinerja Tinggi #
Set adalah koleksi elemen unik yang tidak memperbolehkan adanya duplikasi data. Jika kita mencoba memasukkan nilai yang sudah ada di dalam Set, nilai tersebut akan diabaikan secara otomatis.
Kekuatan utama Set dibandingkan dengan List adalah efisiensi pemrosesan data. Pada List, untuk memeriksa apakah suatu elemen ada di dalamnya (menggunakan metode .contains()), compiler harus memindai seluruh elemen dari awal hingga akhir, yang membutuhkan waktu linear sebesar $O(n)$. Sedangkan pada Set, Dart menggunakan algoritma hashing sehingga pencarian dapat diselesaikan dalam waktu konstan $O(1)$ tidak peduli seberapa banyak data yang ada di dalamnya.
// Inisialisasi Set kosong (Wajib menyertakan tipe data eksplisit)
final uniqueIds = <int>{};
// JANGAN keliru dengan inisialisasi Map kosong yang juga menggunakan kurung kurawal
final emptyMap = {}; // Ini diinterpretasikan sebagai Map<dynamic, dynamic> oleh Dart
// Duplikasi otomatis dibuang
final genders = {'Pria', 'Wanita', 'Pria', 'Wanita'};
print(genders); // Output: {Pria, Wanita}
Operasi Aljabar Himpunan #
Set di Dart dilengkapi dengan metode bawaan untuk melakukan operasi himpunan matematika secara langsung:
final programmer = {'Alice', 'Bob', 'Charlie'};
final designer = {'Charlie', 'Diana', 'Eve'};
// 1. Union: Menggabungkan semua anggota unik
final allEmployees = programmer.union(designer);
// Output: {Alice, Bob, Charlie, Diana, Eve}
// 2. Intersection: Mengambil anggota yang ada di KEDUA kelompok
final hybridStaff = programmer.intersection(designer);
// Output: {Charlie}
// 3. Difference: Mengambil anggota yang HANYA ada di programmer saja
final pureProgrammer = programmer.difference(designer);
// Output: {Alice, Bob}
Map — Struktur Pemetaan Pasangan Kunci dan Nilai #
Map (sering disebut sebagai dictionary di bahasa pemrograman lain) adalah koleksi yang menyimpan data dalam format pasangan kunci (key) dan nilai (value). Setiap kunci di dalam Map bersifat unik dan bertindak sebagai penunjuk indeks untuk mengakses nilai yang terkait.
// Mendefinisikan Map dengan Key bertipe String dan Value bertipe int
final userScores = <String, int>{
'Alice': 95,
'Bob': 80,
};
// Mengakses nilai menggunakan kurung siku []
print(userScores['Alice']); // Output: 95
print(userScores['Charlie']); // Output: null (Jika kunci tidak ditemukan)
Metode Manipulasi Tingkat Lanjut #
Untuk mengelola data di dalam Map secara profesional, hindari penulisan validasi null manual dan mulailah memanfaatkan API bawaan berikut:
final cart = {'apel': 5, 'pisang': 2};
// 1. putIfAbsent: Menambahkan entri baru HANYA jika key belum terdaftar
cart.putIfAbsent('apel', () => 10); // 'apel' tetap bernilai 5 (tidak ditimpa)
cart.putIfAbsent('jeruk', () => 3); // 'jeruk' ditambahkan dengan nilai 3
// 2. update: Memperbarui nilai berdasarkan nilai yang sudah ada sebelumnya
cart.update('pisang', (existingValue) => existingValue + 3); // 'pisang' menjadi 5
// 3. Menangani key yang mungkin belum ada saat update menggunakan parameter ifAbsent
cart.update(
'mangga',
(val) => val + 1,
ifAbsent: () => 1, // Jika 'mangga' belum ada, inisialisasi dengan nilai 1
);
Transformasi Koleksi Menjadi Map #
Kita bisa mengubah koleksi List objek menjadi Map secara idiomatis menggunakan metode Map.fromEntries:
class Product {
final String sku;
final String name;
Product(this.sku, this.name);
}
final productsList = [
Product('SKU-A', 'Laptop'),
Product('SKU-B', 'Keyboard'),
];
// Mengubah List menjadi Map dengan SKU sebagai Key pencari
final ProductMap = Map.fromEntries(
productsList.map((product) => MapEntry(product.sku, product)),
);
print(ProductMap['SKU-A']?.name); // Output: Laptop (Pencarian berjalan O(1))
Operator Modern: Spread, Collection If, dan Collection For #
Dart menyediakan fungsionalitas deklaratif yang sangat kuat langsung di dalam penulisan blok koleksi literal. Fitur-fitur ini sangat penting saat kita menyusun tata letak UI di Flutter.
Spread Operator (... dan ...?)
#
Spread operator digunakan untuk memasukkan seluruh elemen dari suatu koleksi ke dalam koleksi lainnya secara instan.
final baseFeatures = ['Login', 'Register'];
final premiumFeatures = ['Live Chat', 'Analytics'];
// Menggabungkan list
final allFeatures = [...baseFeatures, ...premiumFeatures];
// Null-aware spread operator (...?): Mencegah crash jika list yang disisipkan bernilai null
List<String>? optionalFeatures;
final safeFeatures = ['Home', ...?optionalFeatures, 'Settings'];
// Berjalan aman tanpa memicu Null Pointer Exception
Collection If dan Collection For #
Kedua operator ini memungkinkan kita menyisipkan kondisi logika dan perulangan langsung di dalam deklarasi koleksi:
bool isLoggedIn = true;
final rawNotifications = ['System Update', 'New Promo'];
final dashboardMenu = [
'Home',
'Search',
if (isLoggedIn) 'Profile', // Collection If: Hanya disisipkan jika isLoggedIn bernilai true
for (final notification in rawNotifications) 'Notif: $notification', // Collection For
];
Praktik Terbaik di Flutter: Gunakan Collection If alih-alih operator ternary atau manipulasi list manual saat menambahkan Widget anak secara kondisional. Ini menjaga struktur pohon widget (Widget Tree) kita tetap bersih dan mudah dibaca:
Column( children: [ const HeaderWidget(), if (isNewUser) const WelcomeBannerWidget(), // Sangat bersih! const FooterWidget(), ], )
Koleksi Khusus: Queue dan LinkedList #
Selain tiga koleksi utama di atas, paket pustaka standar Dart (dart:collection) menyediakan dua struktur data khusus untuk skenario optimasi tingkat lanjut:
1. Queue (Antrean Ujung Ganda) #
Queue adalah koleksi yang dirancang khusus untuk melakukan manipulasi penambahan dan penghapusan elemen di awal atau akhir antrean dalam kecepatan konstan $O(1)$. Berbeda dengan List, penyisipan di awal indeks List memerlukan pergeseran memori seluruh elemen lain yang memakan waktu $O(n)$.
import 'dart:collection';
void runQueue() {
final printerQueue = Queue<String>();
printerQueue.addLast('Document_A.pdf'); // Menambah di akhir
printerQueue.addLast('Document_B.pdf');
printerQueue.addFirst('Urgent_Doc.pdf'); // Menambah di paling depan (Prioritas)
// Mengonsumsi antrean dari depan (FIFO)
while (printerQueue.isNotEmpty) {
final doc = printerQueue.removeFirst();
print('Mencetak: $doc');
}
}
2. LinkedList (Senarai Berantai Ganda) #
LinkedList adalah implementasi senarai berantai ganda (doubly linked list). Perlu dicatat bahwa kelas ini tidak mengimplementasikan kelas List bawaan Dart. Setiap elemen yang dimasukkan harus merupakan subclass dari kelas LinkedListEntry.
LinkedList sangat efisien jika kita sering melakukan operasi penyisipan atau penghapusan elemen di tengah-tengah koleksi, karena operasi tersebut dapat diselesaikan dalam waktu $O(1)$ dengan memanfaatkan referensi pointer tetangga tanpa perlu menggeser elemen memori lainnya.
import 'dart:collection';
class TaskEntry extends LinkedListEntry<TaskEntry> {
final String title;
TaskEntry(this.title);
@override
String toString() => title;
}
void runLinkedList() {
final list = LinkedList<TaskEntry>();
final task1 = TaskEntry('Beli Susu');
final task2 = TaskEntry('Cuci Baju');
list.addAll([task1, task2]);
// Menyisipkan tugas baru tepat di tengah secara langsung (O(1))
task1.insertAfter(TaskEntry('Sapu Lantai'));
// Menghapus elemen langsung dari objek entry (O(1))
task2.unlink();
print(list); // Output: (Beli Susu, Sapu Lantai)
}
Pemrograman Fungsional pada Koleksi #
Semua koleksi di Dart yang mengimplementasikan antarmuka Iterable memiliki metode bawaan untuk memproses data dengan paradigma pemrograman fungsional.
Sifat Lazy Evaluation (Evaluasi Tertunda) #
Satu hal krusial yang wajib dipahami tentang operasi fungsional di Dart (seperti map dan where) adalah sifatnya yang lazy (malas/tertunda). Operasi tersebut tidak benar-benar dieksekusi atau melakukan iterasi data saat kita menuliskan kodenya.
Dart hanya membuat penunjuk transformasi baru. Proses iterasi data yang sesungguhnya baru akan berjalan saat kita mengonversi iterable tersebut menjadi koleksi konkret (misalnya dengan memanggil .toList(), .toSet(), atau melakukan loop for-in).
final numbers = [1, 2, 3, 4];
// Operasi map di bawah ini belum mengeksekusi print atau perkalian!
final doubleNumbers = numbers.map((n) {
print('Memproses angka: $n');
return n * 2;
});
print('Langkah pemetaan selesai ditulis.');
// Eksekusi pemrosesan baru berjalan di sini saat toList() dipanggil
final finalResult = doubleNumbers.toList();
Berikut adalah visualisasi bagaimana data mengalir melalui rantai operasi fungsional yang menggabungkan metode penyaringan (where) dan pemetaan (map):
flowchart LR
Input["List: [1, 2, 3, 4]"] --> Filter{"where(isEven)"}
Filter -->|"Hanya Genap"| Mid["Filtered: [2, 4]"]
Mid --> MapOp{"map(x * 10)"}
MapOp --> Output["Output: [20, 40]"]Operasi Agregasi: reduce vs fold #
Kedua metode ini digunakan untuk mereduksi seluruh elemen di dalam koleksi menjadi satu nilai tunggal (misalnya menjumlahkan harga total keranjang belanja).
reduce: Menggunakan elemen pertama sebagai nilai akumulasi awal. Metode ini akan melempar kesalahan StateError jika koleksi dalam keadaan kosong.
final prices = [100, 200, 300];
final totalPrice = prices.reduce((accumulator, element) => accumulator + element);
fold: Membutuhkan nilai awal eksplisit (explicit initial value). Metode ini sangat aman digunakan karena dapat menangani koleksi kosong tanpa memicu crash.
final emptyPrices = <int>[];
// Nilai awal 0 dikirimkan sebagai parameter pertama fold
final safeTotal = emptyPrices.fold(0, (accumulator, element) => accumulator + element);
print(safeTotal); // Output: 0 (Aman dari crash!)
Navigasi dan Pemeriksaan Cepat #
Dart menyediakan metode bawaan yang sangat intuitif untuk menyaring elemen tanpa menulis loop manual:
every: Mengembalikan nilaitruejika seluruh elemen memenuhi kondisi spesifik.any: Mengembalikan nilaitruejika minimal satu elemen memenuhi kondisi spesifik.firstWhere: Mengambil elemen pertama yang memenuhi kondisi, dilengkapi parameterorElseuntuk menangani jika data tidak ditemukan.take(n): Mengambil sejumlahnelemen pertama dari depan.skip(n): Melewati sejumlahnelemen pertama dan mengambil sisanya.
Tabel Kompleksitas Operasi #
Untuk mempermudah optimasi kode aplikasi kita, berikut adalah ringkasan matriks kompleksitas waktu (Big O Notation) untuk setiap jenis koleksi di Dart:
| Operasi | List | Set | Map | Queue | LinkedList |
|---|---|---|---|---|---|
| Akses Indeks / Kunci | $O(1)$ | ✗ | $O(1)$ | ✗ | $O(n)$ |
Pencarian Nilai (contains) | $O(n)$ | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ |
Penyisipan di Akhir (add) | $O(1)$* | $O(1)$ | $O(1)$ | $O(1)$* | $O(1)$ |
| Penyisipan di Awal | $O(n)$ | ✗ | ✗ | $O(1)$ | $O(1)$** |
| Penghapusan di Awal | $O(n)$ | $O(1)$ | $O(1)$ | $O(1)$ | $O(1)$** |
| Penghapusan di Tengah | $O(n)$ | $O(1)$ | $O(1)$ | $O(n)$ | $O(1)$** |
* Nilai waktu rata-rata (amortized). Waktu eksekusi dapat sesekali naik menjadi $O(n)$ saat kapasitas memori buffer penuh dan sistem melakukan resize.
** Operasi berjalan $O(1)$ hanya jika kita telah memegang referensi pointer objek LinkedListEntry yang dituju secara langsung.
Ringkasan #
- List: Koleksi terurut berbasis indeks. Gunakan Growable List untuk kapasitas dinamis dan Fixed-length List / Unmodifiable List untuk efisiensi serta keamanan data.
- Set: Himpunan elemen unik dengan performa pencarian instan $O(1)$. Sangat efisien untuk memproses data non-duplikat dan operasi matematika himpunan.
- Map: Menyimpan data berpasangan Kunci-Nilai. Memiliki metode aman seperti
putIfAbsentdanupdateuntuk memanipulasi entri data.- Operator Modern: Spread Operator (
...) menyederhanakan penggabungan koleksi, sementara Collection If adalah cara terbaik membangun tata letak widget secara kondisional di Flutter.- Queue & LinkedList: Koleksi tingkat lanjut yang dioptimalkan untuk manipulasi ujung antrean (Queue) dan penyisipan/penghapusan tengah instan (LinkedList).
- Pemrograman Fungsional: Operasi seperti
mapdanwheremenggunakan evaluasi tertunda (Lazy Evaluation), menghemat siklus CPU dengan menunda iterasi data hingga benar-benar dibutuhkan.- fold: Selalu pilih
folddibandingkanreducesaat melakukan agregasi data yang berpotensi kosong untuk menghindari runtime exception.