Map

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. Gunakan SplayTreeMap jika butuh kunci terurut, dan HashMap untuk performa murni tanpa peduli urutan.
  • Akses via [] selalu mengembalikan nullable — hasilnya V?, bukan V. Selalu tangani kemungkinan null dengan ??, containsKey, atau cast ! setelah verifikasi.
  • putIfAbsent untuk default value yang mahal — lebih efisien dari pattern if (!map.containsKey(k)) map[k] = expensive() karena hanya memanggil fungsi jika kunci benar-benar belum ada.
  • update dengan ifAbsent untuk 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 dari addAll. Untuk resolusi konflik kustom (misalnya jumlahkan nilai), gunakan update dengan ifAbsent.
  • Collection if dan collection for bekerja di Map literal seperti di List — cara ekspresif membangun Map secara kondisional.
  • fromJson dan toJson wajib untuk model data — jangan gunakan Map<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.
  • UnmodifiableMapView untuk mengekspos Map internal tanpa membuat salinan — lebih hemat memori dari Map.unmodifiable() yang membuat salinan penuh.

← Sebelumnya: List   Berikutnya: Date & Time →

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