Map #
Map is a key-value data structure that provides O(1) average access to every element by its unique key. In Dart, a literal {} containing key: value pairs by default produces a LinkedHashMap — which preserves insertion order, unlike a pure HashMap. Understanding the Map types, safe access patterns, functional transformation, and using Maps as lookup tables will make code working with paired data far more efficient and expressive.
Creating Maps #
// 1. Literal — LinkedHashMap (insertion order preserved)
Map<String, int> skor = {
'Budi': 90,
'Siti': 85,
'Andi': 92,
};
// 2. Empty Map
Map<String, int> kosong = {};
Map<String, dynamic> json = <String, dynamic>{};
// 3. Map.from() — copy from another Map (doesn't preserve the generic type)
Map<String, int> salin = Map.from(skor);
// 4. Map.of() — copy with the generic type preserved (safer)
Map<String, int> salinAman = Map.of(skor);
// 5. Map.fromIterables() — create from two parallel lists
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() — create from a list of MapEntries
Map<String, int> dariEntries = Map.fromEntries([
MapEntry('Budi', 90),
MapEntry('Siti', 85),
]);
// 7. Map.fromIterable() — create from one iterable with key and value functions
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)}
Map Implementation Types #
Dart has three Map implementations with different characteristics:
| Implementation | Order | Lookup | Best for |
|---|---|---|---|
LinkedHashMap | Insertion order | O(1) avg | Default — almost every case |
HashMap | Not guaranteed | O(1) avg | Pure performance, order irrelevant |
SplayTreeMap | Key order (sorted) | O(log n) | Data that needs ordered iteration |
import 'dart:collection';
// LinkedHashMap — default, insertion order preserved
final linked = LinkedHashMap<String, int>();
linked['c'] = 3;
linked['a'] = 1;
linked['b'] = 2;
print(linked.keys.toList()); // ['c', 'a', 'b'] — insertion order
// HashMap — order not guaranteed but can be faster for large datasets
final hash = HashMap<String, int>();
hash['c'] = 3;
hash['a'] = 1;
hash['b'] = 2;
// keys order is not guaranteed
// SplayTreeMap — keys always sorted
final sorted = SplayTreeMap<String, int>();
sorted['c'] = 3;
sorted['a'] = 1;
sorted['b'] = 2;
print(sorted.keys.toList()); // ['a', 'b', 'c'] — always sorted
Access and Modification #
Safe Access #
Access via [] returns null if the key doesn’t exist — it doesn’t throw. This means the result is always nullable and must be handled:
Map<String, int> skor = {'Budi': 90, 'Siti': 85};
// Direct access — returns null if absent
int? nilaiAlice = skor['Alice']; // null — no crash
print(nilaiAlice); // null
// With a ?? fallback
int nilaiDefault = skor['Alice'] ?? 0; // 0 if absent
print(nilaiDefault); // 0
// Correct access for guaranteed values
if (skor.containsKey('Budi')) {
int nilaiPasti = skor['Budi']!; // safe — the key was already checked
print(nilaiPasti); // 90
}
// ANTI-PATTERN: direct access and immediate unboxing without a check
Map<String, int> skor = {'Budi': 90};
int nilai = skor['Alice']!; // ✗ Null check operator on null value — crash!
// CORRECT: check first or use a fallback
int nilai = skor['Alice'] ?? 0; // ✓ fallback
int? nilaiNullable = skor['Alice']; // ✓ nullable
if (skor.containsKey('Alice')) { ... } // ✓ explicit check
Adding and Updating #
Map<String, int> skor = {'Budi': 90};
// Assign — add if new, update if already present
skor['Siti'] = 85; // add a new key
skor['Budi'] = 95; // update an existing value
// putIfAbsent — add ONLY if the key doesn't exist yet
skor.putIfAbsent('Andi', () => 92); // added — didn't exist
skor.putIfAbsent('Budi', () => 0); // IGNORED — already exists
// update — update an existing value (throws if the key is absent)
skor.update('Budi', (lama) => lama + 5); // 95 → 100
// update with ifAbsent — safe for keys that might not exist
skor.update('Rini', (lama) => lama + 5, ifAbsent: () => 80);
// If 'Rini' doesn't exist, the default value 80 is used
Removing #
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
int? dihapus = skor.remove('Budi'); // removes and returns its value
print(dihapus); // 90
print(skor); // {'Siti': 85, 'Andi': 92}
skor.removeWhere((kunci, nilai) => nilai < 90); // remove all with value < 90
print(skor); // {'Andi': 92}
skor.clear(); // remove everything
print(skor); // {}
Iterating Maps #
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
// Iterating entries — the most idiomatic way
for (final entry in skor.entries) {
print('${entry.key}: ${entry.value}');
}
// Iterating with forEach — useful for simple actions
skor.forEach((nama, nilai) => print('$nama got $nilai'));
// Iterating only keys
for (final nama in skor.keys) {
print(nama);
}
// Iterating only values
for (final nilai in skor.values) {
print(nilai);
}
// Converting to a List for index-based iteration
final entries = skor.entries.toList();
for (int i = 0; i < entries.length; i++) {
print('${i + 1}. ${entries[i].key}: ${entries[i].value}');
}
Transforming Maps #
Map supports functional transformation via map() on entries, producing a new Map:
Map<String, int> skor = {'Budi': 90, 'Siti': 85, 'Andi': 92};
// map() on entries — transform keys and/or values
Map<String, String> grade = skor.map(
(nama, nilai) => MapEntry(nama, nilai >= 90 ? 'A' : 'B'),
);
// {'Budi': 'A', 'Siti': 'B', 'Andi': 'A'}
// Change all values
Map<String, double> nilaiDouble = skor.map(
(k, v) => MapEntry(k, v / 100.0),
);
// {'Budi': 0.9, 'Siti': 0.85, 'Andi': 0.92}
// Change all keys (e.g. lowercase)
Map<String, int> lowercase = skor.map(
(k, v) => MapEntry(k.toLowerCase(), v),
);
// Filter using entries (no direct where on Map)
Map<String, int> lulusSaja = Map.fromEntries(
skor.entries.where((e) => e.value >= 90),
);
// {'Budi': 90, 'Andi': 92}
List ↔ Map Conversion #
// List to Map — with asMap() for indices as keys
List<String> buah = ['apel', 'jeruk', 'mangga'];
Map<int, String> denganIndeks = buah.asMap();
// {0: 'apel', 1: 'jeruk', 2: 'mangga'}
// Object List to a lookup Map
List<Produk> produk = [...];
Map<String, Produk> produkById = {
for (final p in produk) p.id: p,
};
// Map to List
List<MapEntry<String, int>> entries = skor.entries.toList();
List<String> daftarNama = skor.keys.toList();
List<int> daftarNilai = skor.values.toList();
// Map to a List of objects with transformation
List<String> ringkasan = skor.entries
.map((e) => '${e.key}: ${e.value}')
.toList();
Maps as Lookup Tables #
One of the most impactful uses of Map: replacing linear O(n) searches with O(1) lookups.
// ANTI-PATTERN: repeated linear search — O(n) per lookup
List<Produk> produk = ambilSemuaProduk(); // 10,000 products
for (final orderId in orderIds) { // 1,000 orders
final idProduk = ambilIdProduk(orderId);
// O(n) for every search!
final p = produk.firstWhere((p) => p.id == idProduk);
prosesOrder(orderId, p);
}
// Total: O(n × m) = O(10,000 × 1,000) = O(10,000,000) — very slow!
// CORRECT: build a lookup table first, then query in O(1)
List<Produk> produk = ambilSemuaProduk();
// Build the Map once — 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) — far faster
Caching with Map #
// Memoization — cache expensive computation results
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';
// Return from the cache if present
if (_cache.containsKey(kunci)) {
return _cache[kunci]!;
}
// Compute and store in the cache
final kurs = await _api.ambilKurs(dari, ke);
_cache[kunci] = kurs;
return kurs;
}
void bersihkanCache() => _cache.clear();
}
Maps for JSON Data #
Map<String, dynamic> is the standard type for JSON data in Dart. Understanding how to work with nested Maps is essential for apps consuming APIs:
// Parsed JSON response
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,
};
// Access values with safe casts
String id = responseJson['id'] as String;
int umur = responseJson['umur'] as int;
bool aktif = responseJson['aktif'] as bool;
// Access a nested Map
Map<String, dynamic> alamat = responseJson['alamat'] as Map<String, dynamic>;
String kota = alamat['kota'] as String;
// Access a List from JSON
List<dynamic> hobiRaw = responseJson['hobi'] as List<dynamic>;
List<String> hobi = hobiRaw.cast<String>();
// Access a nullable field
String? metadata = responseJson['metadata'] as String?;
// ANTI-PATTERN: JSON access without type safety
Map<String, dynamic> data = ambilDariApi();
String nama = data['nama']; // ✗ String? can't be assigned to String
print(data['alamat']['kota']); // ✗ can crash if 'alamat' is null
int? nilai = data['skor']; // ✗ double can't be cast to int?
// CORRECT: explicit casts and proper nullable handling
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 covers both int and double
The fromJson and toJson Pattern
#
For frequently used data models, always create fromJson and toJson methods:
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 a list of objects
List<Map<String, dynamic>> rawList = response['produk'] as List<dynamic>;
List<Produk> produk = rawList.map(Produk.fromJson).toList();
Spread and Collection If/For on Maps #
Like List, Map also supports spread operators and collection if/for:
Map<String, int> dasar = {'apel': 1, 'jeruk': 2};
Map<String, int> tambahan = {'mangga': 3, 'pisang': 4};
// Spread — combine Maps (same key: right value wins)
Map<String, int> gabung = {...dasar, ...tambahan};
// {'apel': 1, 'jeruk': 2, 'mangga': 3, 'pisang': 4}
// Duplicate keys — the last value wins
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 skipped if null
// Collection if
bool adalahAdmin = true;
Map<String, dynamic> konfigurasi = {
'tema': 'gelap',
'bahasa': 'id',
if (adalahAdmin) 'panelAdmin': true,
if (adalahAdmin) 'debugMode': false,
};
// Collection for — build a Map from a List
List<String> kodeNegara = ['ID', 'MY', 'SG'];
Map<String, String> namaLengkap = {
for (final kode in kodeNegara)
kode: _ambilNamaLengkap(kode),
};
Merging Maps with Conflict Resolution #
When merging Maps that may share keys, you need a conflict resolution strategy:
Map<String, int> a = {'x': 1, 'y': 2};
Map<String, int> b = {'y': 20, 'z': 30}; // 'y' is in both
// Spread — right value wins for duplicate keys
Map<String, int> kananMenang = {...a, ...b}; // {'x': 1, 'y': 20, 'z': 30}
Map<String, int> kiriMenang = {...b, ...a}; // {'y': 2, 'z': 30, 'x': 1}
// Custom strategy — sum conflicting values
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} — the 'y' value is summed
Unmodifiable Maps #
Just like List, Maps returned from public APIs should be wrapped so they can’t be modified from outside:
import 'dart:collection';
class KonfigurasiAplikasi {
final Map<String, dynamic> _config;
KonfigurasiAplikasi(Map<String, dynamic> config)
: _config = Map.of(config); // make a copy
// UnmodifiableMapView — a wrapper without copying the data (memory efficient)
Map<String, dynamic> get semua => UnmodifiableMapView(_config);
// Or Map.unmodifiable — makes a truly immutable copy
Map<String, dynamic> get snapshot => Map.unmodifiable(_config);
dynamic ambil(String kunci, {dynamic defaultValue}) =>
_config[kunci] ?? defaultValue;
}
Map Anti-Patterns to Avoid #
Map as a Replacement for a Model Class #
// ANTI-PATTERN: using Map<String, dynamic> as a permanent data model
Map<String, dynamic> pengguna = {
'nama': 'Budi',
'email': '[email protected]',
'umur': 25,
};
// No type safety — typos aren't detected at compile time
print(pengguna['Nama']); // ✗ null — typo 'Nama' vs 'nama', no error
print(pengguna['email'].toUpperCase()); // ✗ can crash if null
// CORRECT: use a class with fromJson for models used repeatedly
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,
);
}
// Access with compile-time safety
final p = Pengguna.fromJson(data);
print(p.nama.toUpperCase()); // ✓ type-safe
Modifying a Map While Iterating #
Map<String, int> skor = {'Budi': 90, 'Siti': 50, 'Andi': 85};
// ANTI-PATTERN: modifying a Map while iterating — ConcurrentModificationError
for (final kunci in skor.keys) {
if (skor[kunci]! < 60) {
skor.remove(kunci); // ✗ ConcurrentModificationError!
}
}
// CORRECT: collect the keys to remove, remove after iteration
final kunciYangDihapus = skor.keys.where((k) => skor[k]! < 60).toList();
kunciYangDihapus.forEach(skor.remove);
// Or build a new filtered Map — more idiomatic
skor = Map.fromEntries(skor.entries.where((e) => e.value >= 60));
Ignoring the remove Return Value
#
Map<String, String> cache = {'key1': 'val1', 'key2': 'val2'};
// ANTI-PATTERN: checking and removing separately — two operations
bool ada = cache.containsKey('key1');
cache.remove('key1');
// If another thread also modifies between the two — race condition
// CORRECT: remove returns the removed value — take advantage of it
String? dihapus = cache.remove('key1'); // remove and get the value at once
if (dihapus != null) {
print('Removed: $dihapus'); // use the value
}
Summary #
- The default Map in Dart is
LinkedHashMap— preserving insertion order. UseSplayTreeMapif you need sorted keys, andHashMapfor pure performance without caring about order.- Access via
[]always returns a nullable — the result isV?, notV. Always handle the possibility of null with??,containsKey, or a!cast after verification.putIfAbsentfor expensive default values — more efficient than theif (!map.containsKey(k)) map[k] = expensive()pattern because it only calls the function when the key truly doesn’t exist.updatewithifAbsentfor accumulation — ideal for counting frequencies, summing values per group, or common counter patterns.- Map as a lookup table turns O(n) searches into O(1) — build the Map once up front, query many times. This is the highest-impact performance optimization for code processing large data.
- Spread
{...m1, ...m2}for merging Maps — more idiomatic thanaddAll. For custom conflict resolution (e.g. summing values), useupdatewithifAbsent.- Collection if and collection for work in Map literals just like in List — an expressive way to build Maps conditionally.
fromJsonandtoJsonare mandatory for data models — don’t useMap<String, dynamic>as the permanent representation for business objects. Key typos aren’t detected at compile time and can become hard-to-trace bug sources.- Don’t modify a Map while iterating — collect the keys that need changing/removing, then modify after iteration finishes, or build a new filtered Map.
UnmodifiableMapViewfor exposing an internal Map without making a copy — more memory efficient thanMap.unmodifiable(), which makes a full copy.