Map #
Map adalah struktur data kunci-nilai yang memberikan akses O(1) rata-rata ke setiap elemen berdasarkan kunci uniknya. Di Dart, literal {} yang berisi pasangan kunci: nilai secara default menghasilkan LinkedHashMap — yang menjaga urutan penyisipan, berbeda dari HashMap murni. Pemahaman tentang jenis-jenis Map, cara akses yang aman, transformasi fungsional, dan penggunaan Map sebagai lookup table akan membuat kode yang bekerja dengan data berpasangan jauh lebih efisien dan ekspresif.
Membuat Map #
// 1. Literal — LinkedHashMap (urutan penyisipan dijaga)
Map<String, int> skor = {
'Budi': 90,
'Siti': 85,
'Andi': 92,
};
// 2. Map kosong
Map<String, int> kosong = {};
Map<String, dynamic> json = <String, dynamic>{};
// 3. Map.from() — salinan dari Map lain (tidak mempertahankan tipe generik)
Map<String, int> salin = Map.from(skor);
// 4. Map.of() — salinan dengan tipe generik dipertahankan (lebih aman)
Map<String, int> salinAman = Map.of(skor);
// 5. Map.fromIterables() — buat dari dua list paralel
List<String> nama = ['Budi', 'Siti', 'Andi'];
List<int> nilai = [90, 85, 92];
Map<String, int> dariIterables = Map.fromIterables(nama, nilai);
// {'Budi': 90, 'Siti': 85, 'Andi': 92}
// 6. Map.fromEntries() — buat dari list MapEntry
Map<String, int> dariEntries = Map.fromEntries([
MapEntry('Budi', 90),
MapEntry('Siti', 85),
]);
// 7. Map.fromIterable() — buat dari satu iterable dengan fungsi kunci dan nilai
List<Pengguna> pengguna = [Pengguna('U001', 'Budi'), Pengguna('U002', 'Siti')];
Map<String, Pengguna> indeksPengguna = Map.fromIterable(
pengguna,
key: (p) => p.id,
value: (p) => p,
);
// {'U001': Pengguna(U001, Budi), 'U002': Pengguna(U002, Siti)}
Jenis Implementasi Map #
Dart memiliki tiga implementasi Map yang berbeda karakteristik:
| Implementasi | Urutan | Lookup | Cocok untuk |
|---|---|---|---|
LinkedHashMap |
Urutan penyisipan | O(1) avg | Default — hampir semua kasus |
HashMap |
Tidak terjamin | O(1) avg | Performa murni, urutan tidak penting |
SplayTreeMap |
Urutan kunci (sorted) | O(log n) | Data yang perlu diiterasi terurut |
import 'dart:collection';
// LinkedHashMap — default, urutan penyisipan dijaga
final linked = LinkedHashMap<String, int>();
linked['c'] = 3;
linked['a'] = 1;
linked['b'] = 2;
print(linked.keys.toList()); // ['c', 'a', 'b'] — urutan penyisipan
// HashMap — urutan tidak dijamin tapi bisa lebih cepat untuk dataset besar
final hash = HashMap<String, int>();
hash['c'] = 3;
hash['a'] = 1;
hash['b'] = 2;
// urutan keys tidak terjamin
// SplayTreeMap — kunci selalu terurut
final sorted = SplayTreeMap<String, int>();
sorted['c'] = 3;
sorted['a'] = 1;
sorted['b'] = 2;
print(sorted.keys.toList()); // ['a', 'b', 'c'] — selalu terurut
Akses dan Modifikasi #
Akses yang Aman #
Akses via [] mengembalikan null jika kunci tidak ada — tidak throw. Ini berarti hasilnya selalu nullable dan harus ditangani:
Map<String, int> skor = {'Budi': 90, 'Siti': 85};
// Akses langsung — mengembalikan null jika tidak ada
int? nilaiAlice = skor['Alice']; // null — tidak crash
print(nilaiAlice); // null
// Dengan fallback menggunakan ??
int nilaiDefault = skor['Alice'] ?? 0; // 0 jika tidak ada
print(nilaiDefault); // 0
// Akses yang benar untuk nilai yang dijamin ada
if (skor.containsKey('Budi')) {
int nilaiPasti = skor['Budi']!; // aman — kunci sudah dicek
print(nilaiPasti); // 90
}
// ANTI-PATTERN: akses langsung dan langsung unbox tanpa cek
Map<String, int> skor = {'Budi': 90};
int nilai = skor['Alice']!; // ✗ Null check operator on null value — crash!
// BENAR: cek dulu atau gunakan fallback
int nilai = skor['Alice'] ?? 0; // ✓ fallback
int? nilaiNullable = skor['Alice']; // ✓ nullable
if (skor.containsKey('Alice')) { ... } // ✓ explicit check
Menambah dan Memperbarui #
Map<String, int> skor = {'Budi': 90};
// Assign — tambah jika baru, update jika sudah ada
skor['Siti'] = 85; // tambah kunci baru
skor['Budi'] = 95; // update nilai yang ada
// putIfAbsent — tambah HANYA jika kunci belum ada
skor.putIfAbsent('Andi', () => 92); // ditambahkan — belum ada
skor.putIfAbsent('Budi', () => 0); // DIABAIKAN — sudah ada
// update — perbarui nilai yang ada (throw jika kunci tidak ada)
skor.update('Budi', (lama) => lama + 5); // 95 → 100
// update dengan ifAbsent — aman untuk kunci yang mungkin tidak ada
skor.update('Rini', (lama) => lama + 5, ifAbsent: () => 80);
// Jika 'Rini' tidak ada, nilai default 80 digunakan
Menghapus #
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
int? dihapus = skor.remove('Budi'); // menghapus dan mengembalikan nilainya
print(dihapus); // 90
print(skor); // {'Siti': 85, 'Andi': 92}
skor.removeWhere((kunci, nilai) => nilai < 90); // hapus semua yang nilainya < 90
print(skor); // {'Andi': 92}
skor.clear(); // hapus semua
print(skor); // {}
Iterasi Map #
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
// Iterasi entries — cara paling idiomatis
for (final entry in skor.entries) {
print('${entry.key}: ${entry.value}');
}
// Iterasi dengan forEach — berguna untuk aksi sederhana
skor.forEach((nama, nilai) => print('$nama mendapat $nilai'));
// Iterasi hanya kunci
for (final nama in skor.keys) {
print(nama);
}
// Iterasi hanya nilai
for (final nilai in skor.values) {
print(nilai);
}
// Konversi ke List untuk iterasi dengan indeks
final entries = skor.entries.toList();
for (int i = 0; i < entries.length; i++) {
print('${i + 1}. ${entries[i].key}: ${entries[i].value}');
}
Transformasi Map #
Map mendukung transformasi fungsional melalui map() pada entries, yang menghasilkan Map baru:
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
// map() pada entries — transformasi kunci dan/atau nilai
Map<String, String> grade = skor.map(
(nama, nilai) => MapEntry(nama, nilai >= 90 ? 'A' : 'B'),
);
// {'Budi': 'A', 'Siti': 'B', 'Andi': 'A'}
// Ubah semua nilai
Map<String, double> nilaiDouble = skor.map(
(k, v) => MapEntry(k, v / 100.0),
);
// {'Budi': 0.9, 'Siti': 0.85, 'Andi': 0.92}
// Ubah semua kunci (misal: lowercase)
Map<String, int> lowercase = skor.map(
(k, v) => MapEntry(k.toLowerCase(), v),
);
// Filter menggunakan entries (tidak ada where langsung di Map)
Map<String, int> lulusSaja = Map.fromEntries(
skor.entries.where((e) => e.value >= 90),
);
// {'Budi': 90, 'Andi': 92}
Konversi List ↔ Map #
// List ke Map — dengan asMap() untuk indeks sebagai kunci
List<String> buah = ['apel', 'jeruk', 'mangga'];
Map<int, String> denganIndeks = buah.asMap();
// {0: 'apel', 1: 'jeruk', 2: 'mangga'}
// List objek ke Map lookup
List<Produk> produk = [...];
Map<String, Produk> produkById = {
for (final p in produk) p.id: p,
};
// Map ke List
List<MapEntry<String, int>> entries = skor.entries.toList();
List<String> daftarNama = skor.keys.toList();
List<int> daftarNilai = skor.values.toList();
// Map ke List of objects dengan transformasi
List<String> ringkasan = skor.entries
.map((e) => '${e.key}: ${e.value}')
.toList();
Map sebagai Lookup Table #
Salah satu penggunaan Map yang paling berdampak pada performa: menggantikan pencarian linear O(n) dengan lookup O(1).
// ANTI-PATTERN: pencarian linear berulang — O(n) per lookup
List<Produk> produk = ambilSemuaProduk(); // 10.000 produk
for (final orderId in orderIds) { // 1.000 order
final idProduk = ambilIdProduk(orderId);
// O(n) untuk setiap pencarian!
final p = produk.firstWhere((p) => p.id == idProduk);
prosesOrder(orderId, p);
}
// Total: O(n × m) = O(10.000 × 1.000) = O(10.000.000) — sangat lambat!
// BENAR: bangun lookup table dulu, lalu query O(1)
List<Produk> produk = ambilSemuaProduk();
// Bangun Map sekali — O(n)
Map<String, Produk> produkMap = {for (final p in produk) p.id: p};
for (final orderId in orderIds) {
final idProduk = ambilIdProduk(orderId);
final p = produkMap[idProduk]; // O(1)!
if (p != null) prosesOrder(orderId, p);
}
// Total: O(n + m) — jauh lebih cepat
Cache dengan Map #
// Memoization — cache hasil komputasi mahal
class KonversiMata {
final Map<String, double> _cache = {};
final ApiKurs _api;
KonversiMata(this._api);
Future<double> konversi(String dari, String ke) async {
final kunci = '$dari-$ke';
// Kembalikan dari cache jika ada
if (_cache.containsKey(kunci)) {
return _cache[kunci]!;
}
// Hitung dan simpan ke cache
final kurs = await _api.ambilKurs(dari, ke);
_cache[kunci] = kurs;
return kurs;
}
void bersihkanCache() => _cache.clear();
}
Map untuk Data JSON #
Map<String, dynamic> adalah tipe standar untuk data JSON di Dart. Pemahaman cara bekerja dengan nested Map sangat penting untuk aplikasi yang mengonsumsi API:
// JSON response yang diparse
Map<String, dynamic> responseJson = {
'id': 'U001',
'nama': 'Budi Santoso',
'umur': 25,
'aktif': true,
'skor': 92.5,
'alamat': {
'jalan': 'Jl. Merdeka No. 1',
'kota': 'Jakarta',
'kodePos': '10110',
},
'hobi': ['membaca', 'coding', 'olahraga'],
'metadata': null,
};
// Akses nilai dengan cast yang aman
String id = responseJson['id'] as String;
int umur = responseJson['umur'] as int;
bool aktif = responseJson['aktif'] as bool;
// Akses nested Map
Map<String, dynamic> alamat = responseJson['alamat'] as Map<String, dynamic>;
String kota = alamat['kota'] as String;
// Akses List dari JSON
List<dynamic> hobiRaw = responseJson['hobi'] as List<dynamic>;
List<String> hobi = hobiRaw.cast<String>();
// Akses nullable field
String? metadata = responseJson['metadata'] as String?;
// ANTI-PATTERN: akses JSON tanpa type safety
Map<String, dynamic> data = ambilDariApi();
String nama = data['nama']; // ✗ String? tidak bisa diassign ke String
print(data['alamat']['kota']); // ✗ bisa crash jika 'alamat' null
int? nilai = data['skor']; // ✗ double tidak bisa dicast ke int?
// BENAR: cast eksplisit dan tangani nullable dengan benar
String nama = data['nama'] as String;
Map<String, dynamic>? alamatMap = data['alamat'] as Map<String, dynamic>?;
String? kota = alamatMap?['kota'] as String?;
double skor = (data['skor'] as num).toDouble(); // num mencakup int dan double
Pola fromJson dan toJson
#
Untuk model data yang sering digunakan, selalu buat method fromJson dan toJson:
class Produk {
final String id;
final String nama;
final double harga;
final int stok;
final List<String> tag;
const Produk({
required this.id,
required this.nama,
required this.harga,
required this.stok,
this.tag = const [],
});
factory Produk.fromJson(Map<String, dynamic> json) {
return Produk(
id: json['id'] as String,
nama: json['nama'] as String,
harga: (json['harga'] as num).toDouble(),
stok: json['stok'] as int,
tag: (json['tag'] as List<dynamic>?)?.cast<String>() ?? [],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'nama': nama,
'harga': harga,
'stok': stok,
'tag': tag,
};
}
// Parsing list of objects
List<Map<String, dynamic>> rawList = response['produk'] as List<dynamic>;
List<Produk> produk = rawList.map(Produk.fromJson).toList();
Spread dan Collection If/For pada Map #
Seperti List, Map juga mendukung spread operator dan collection if/for:
Map<String, int> dasar = {'apel': 1, 'jeruk': 2};
Map<String, int> tambahan = {'mangga': 3, 'pisang': 4};
// Spread — gabungkan Map (kunci yang sama: nilai kanan menang)
Map<String, int> gabung = {...dasar, ...tambahan};
// {'apel': 1, 'jeruk': 2, 'mangga': 3, 'pisang': 4}
// Kunci duplikat — nilai terakhir menang
Map<String, int> override = {...dasar, 'apel': 99};
// {'apel': 99, 'jeruk': 2}
// Null-aware spread
Map<String, int>? opsional;
Map<String, int> aman = {...dasar, ...?opsional}; // opsional diabaikan jika null
// Collection if
bool adalahAdmin = true;
Map<String, dynamic> konfigurasi = {
'tema': 'gelap',
'bahasa': 'id',
if (adalahAdmin) 'panelAdmin': true,
if (adalahAdmin) 'debugMode': false,
};
// Collection for — bangun Map dari List
List<String> kodeNegara = ['ID', 'MY', 'SG'];
Map<String, String> namaLengkap = {
for (final kode in kodeNegara)
kode: _ambilNamaLengkap(kode),
};
Menggabungkan Map dengan Resolusi Konflik #
Saat menggabungkan Map yang mungkin memiliki kunci yang sama, perlu strategi resolusi konflik:
Map<String, int> a = {'x': 1, 'y': 2};
Map<String, int> b = {'y': 20, 'z': 30}; // 'y' ada di keduanya
// Spread — nilai kanan menang untuk kunci duplikat
Map<String, int> kananMenang = {...a, ...b}; // {'x': 1, 'y': 20, 'z': 30}
Map<String, int> kiriMenang = {...b, ...a}; // {'y': 2, 'z': 30, 'x': 1}
// Strategi kustom — jumlahkan nilai yang konflik
Map<String, int> gabungDenganJumlah(
Map<String, int> m1, Map<String, int> m2) {
final hasil = Map.of(m1);
m2.forEach((kunci, nilai) {
hasil.update(kunci, (lama) => lama + nilai, ifAbsent: () => nilai);
});
return hasil;
}
final total = gabungDenganJumlah(a, b);
// {'x': 1, 'y': 22, 'z': 30} — nilai 'y' dijumlahkan
Map yang Tidak Bisa Dimodifikasi #
Sama seperti List, Map yang dikembalikan dari API publik sebaiknya dibungkus agar tidak bisa dimodifikasi dari luar:
import 'dart:collection';
class KonfigurasiAplikasi {
final Map<String, dynamic> _config;
KonfigurasiAplikasi(Map<String, dynamic> config)
: _config = Map.of(config); // buat salinan
// UnmodifiableMapView — wrapper tanpa menyalin data (hemat memori)
Map<String, dynamic> get semua => UnmodifiableMapView(_config);
// Atau Map.unmodifiable — membuat salinan yang benar-benar immutable
Map<String, dynamic> get snapshot => Map.unmodifiable(_config);
dynamic ambil(String kunci, {dynamic defaultValue}) =>
_config[kunci] ?? defaultValue;
}
Anti-Pattern Map yang Harus Dihindari #
Map sebagai Pengganti Kelas Model #
// ANTI-PATTERN: menggunakan Map<String, dynamic> sebagai model data permanen
Map<String, dynamic> pengguna = {
'nama': 'Budi',
'email': '[email protected]',
'umur': 25,
};
// Tidak ada type safety — typo tidak terdeteksi saat compile
print(pengguna['Nama']); // ✗ null — typo 'Nama' vs 'nama', tidak ada error
print(pengguna['email'].toUpperCase()); // ✗ bisa crash jika null
// BENAR: gunakan kelas dengan fromJson untuk model yang digunakan berulang
class Pengguna {
final String nama;
final String email;
final int umur;
const Pengguna({required this.nama, required this.email, required this.umur});
factory Pengguna.fromJson(Map<String, dynamic> json) => Pengguna(
nama: json['nama'] as String,
email: json['email'] as String,
umur: json['umur'] as int,
);
}
// Akses dengan compile-time safety
final p = Pengguna.fromJson(data);
print(p.nama.toUpperCase()); // ✓ type-safe
Iterasi Map Sambil Memodifikasi #
Map<String, int> skor = {'Budi': 90, 'Siti': 50, 'Andi': 85};
// ANTI-PATTERN: modifikasi Map saat iterasi — ConcurrentModificationError
for (final kunci in skor.keys) {
if (skor[kunci]! < 60) {
skor.remove(kunci); // ✗ ConcurrentModificationError!
}
}
// BENAR: kumpulkan kunci yang akan dihapus, hapus setelah iterasi
final kunciYangDihapus = skor.keys.where((k) => skor[k]! < 60).toList();
kunciYangDihapus.forEach(skor.remove);
// Atau buat Map baru yang difilter — lebih idiomatis
skor = Map.fromEntries(skor.entries.where((e) => e.value >= 60));
Mengabaikan Return Value remove
#
Map<String, String> cache = {'key1': 'val1', 'key2': 'val2'};
// ANTI-PATTERN: hapus dan akses terpisah — dua operasi
bool ada = cache.containsKey('key1');
cache.remove('key1');
// Jika thread lain juga memodifikasi di antara keduanya — race condition
// BENAR: remove mengembalikan nilai yang dihapus — manfaatkan ini
String? dihapus = cache.remove('key1'); // hapus dan dapatkan nilainya sekaligus
if (dihapus != null) {
print('Dihapus: $dihapus'); // gunakan nilainya
}
Ringkasan #
- Default Map di Dart adalah
LinkedHashMap— menjaga urutan penyisipan. GunakanSplayTreeMapjika butuh kunci terurut, danHashMapuntuk performa murni tanpa peduli urutan.- Akses via
[]selalu mengembalikan nullable — hasilnyaV?, bukanV. Selalu tangani kemungkinan null dengan??,containsKey, atau cast!setelah verifikasi.putIfAbsentuntuk default value yang mahal — lebih efisien dari patternif (!map.containsKey(k)) map[k] = expensive()karena hanya memanggil fungsi jika kunci benar-benar belum ada.updatedenganifAbsentuntuk akumulasi — ideal untuk menghitung frekuensi, menjumlahkan nilai per grup, atau pola counter umum.- Map sebagai lookup table mengubah pencarian O(n) menjadi O(1) — bangun Map sekali di awal, query berkali-kali. Ini adalah optimasi performa paling berdampak untuk kode yang memproses data besar.
- Spread
{...m1, ...m2}untuk menggabungkan Map — lebih idiomatis dariaddAll. Untuk resolusi konflik kustom (misalnya jumlahkan nilai), gunakanupdatedenganifAbsent.- Collection if dan collection for bekerja di Map literal seperti di List — cara ekspresif membangun Map secara kondisional.
fromJsondantoJsonwajib untuk model data — jangan gunakanMap<String, dynamic>sebagai representasi permanen untuk objek bisnis. Typo pada kunci tidak terdeteksi saat compile dan bisa menjadi sumber bug yang sulit dilacak.- Jangan modifikasi Map saat iterasi — kumpulkan kunci yang perlu diubah/dihapus, lalu lakukan modifikasi setelah iterasi selesai, atau buat Map baru dengan filter.
UnmodifiableMapViewuntuk mengekspos Map internal tanpa membuat salinan — lebih hemat memori dariMap.unmodifiable()yang membuat salinan penuh.