List #
List is an ordered collection accessible by index — the most frequently used data structure in almost every Dart program. Behind its simplicity, Dart’s List hides several important design decisions worth understanding: the difference between growable and fixed-length lists, when a List should be replaced with a more memory-efficient Iterable, how functional methods like map, where, and fold work lazily, and why using + to concatenate lists in a loop can become a serious performance problem. This article covers all of it — from the basics to the patterns used in real production applications.
Creating Lists #
There are several ways to create a List in Dart, each with different characteristics:
// 1. Literal — the most common way
List<int> angka = [1, 2, 3, 4, 5];
List<String> kota = ['Jakarta', 'Bandung', 'Surabaya'];
List<dynamic> campuran = [1, 'dua', true, null]; // avoid this
// 2. List.empty() — empty list
List<String> kosong = []; // growable (default)
List<String> kosongGrowable = List.empty(growable: true);
// 3. List.filled() — fill all elements with the same value
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() — fill with a generator function
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() — create from another Iterable
List<int> dariSet = List.from({3, 1, 4, 1, 5}); // Set to List
List<int> dariRange = List.from(Iterable.generate(5)); // [0, 1, 2, 3, 4]
// 6. List.of() — like List.from() but preserves the generic type
List<num> nums = [1, 2.5, 3];
List<num> salin = List.of(nums); // ✓ type preserved
Fixed-Length vs Growable #
// Growable — elements can be added/removed (default)
List<int> growable = [1, 2, 3];
growable.add(4); // ✓
growable.remove(2); // ✓
// Fixed-length — size is fixed after creation
List<int> fixed = List.filled(3, 0); // [0, 0, 0]
fixed[0] = 10; // ✓ values can be changed
fixed.add(4); // ✗ UnsupportedError: can't add elements
// Const — truly immutable: size AND values can't be changed
const List<int> konstanta = [1, 2, 3];
konstanta.add(4); // ✗ UnsupportedError
konstanta[0] = 99; // ✗ UnsupportedError
Access and Basic Properties #
List<String> buah = ['apel', 'jeruk', 'mangga', 'pisang', 'anggur'];
// Access by index
print(buah[0]); // 'apel' — first index
print(buah[buah.length - 1]); // 'anggur' — last index
// Properties
print(buah.first); // 'apel'
print(buah.last); // 'anggur'
print(buah.length); // 5
print(buah.isEmpty); // false
print(buah.isNotEmpty); // true
// Search
print(buah.contains('mangga')); // true
print(buah.indexOf('jeruk')); // 1
print(buah.lastIndexOf('apel')); // 0
print(buah.indexWhere((b) => b.startsWith('a'))); // 0
// Safe access — avoid RangeError
String? pertama = buah.firstOrNull; // 'apel'
String? tidakAda = buah.firstWhereOrNull((b) => b == 'durian'); // null
// ANTI-PATTERN: accessing an index without checking
List<String> hasil = ambilData();
print(hasil[0]); // ✗ RangeError if the list is empty
// CORRECT: check first or use firstOrNull
if (hasil.isNotEmpty) {
print(hasil.first); // ✓
}
// or
print(hasil.firstOrNull ?? 'kosong'); // ✓ null-safe
Modifying Lists #
Adding Elements #
List<int> angka = [1, 2, 3];
angka.add(4); // add one at the end: [1, 2, 3, 4]
angka.addAll([5, 6, 7]); // add many at the end: [1, 2, 3, 4, 5, 6, 7]
angka.insert(0, 0); // insert at index 0: [0, 1, 2, 3, 4, 5, 6, 7]
angka.insertAll(1, [-2, -1]); // insert several: [0, -2, -1, 1, 2, ...]
// Spread operator — the idiomatic way to combine
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]; // skipped if null
Removing Elements #
List<String> kota = ['Jakarta', 'Bandung', 'Surabaya', 'Bandung', 'Medan'];
kota.remove('Bandung'); // remove the FIRST occurrence: ['Jakarta', 'Surabaya', 'Bandung', 'Medan']
kota.removeAt(0); // remove at index 0: ['Surabaya', 'Bandung', 'Medan']
kota.removeLast(); // remove the last: ['Surabaya', 'Bandung']
kota.removeWhere((k) => k.length > 7); // remove all with length > 7
kota.retainWhere((k) => k.startsWith('S')); // keep those starting with 'S'
kota.clear(); // remove all elements
Changing Elements #
List<int> angka = [1, 2, 3, 4, 5];
angka[2] = 99; // change the element at index 2
angka.setAll(1, [20, 30]); // change several starting at index 1: [1, 20, 30, 4, 5]
angka.fillRange(0, 3, 0); // fill the range [0, 3) with 0: [0, 0, 0, 4, 5]
angka.replaceRange(1, 3, [10, 20, 30]); // replace the range [1, 3) with a new list
Functional Methods — List’s Real Power #
Functional methods work lazily on Iterable — results aren’t computed until iterated. Call .toList() at the end to get a fully evaluated List.
map — Transforming Every Element
#
List<int> angka = [1, 2, 3, 4, 5];
// map returns an Iterable<T> — lazy
Iterable<int> kuadrat = angka.map((n) => n * n);
// toList() to get a List<T>
List<int> kuadratList = angka.map((n) => n * n).toList();
// [1, 4, 9, 16, 25]
// Transforming to a different type
List<String> diformat = angka.map((n) => 'Nilai: $n').toList();
// ['Nilai: 1', 'Nilai: 2', ...]
// Transforming objects
List<Produk> produk = rawData.map(Produk.dariJson).toList();
where — Filtering Elements
#
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 and fold — Aggregation
#
List<int> angka = [1, 2, 3, 4, 5];
// reduce — throws if the list is empty
int jumlah = angka.reduce((acc, n) => acc + n); // 15
int maks = angka.reduce((a, b) => a > b ? a : b); // 5
// fold — safe for empty lists, can return a different type
int jumlahFold = angka.fold(0, (acc, n) => acc + n); // 15
String digabung = angka.fold('', (acc, n) => '$acc$n'); // '12345'
// Computing an average
double rataRata = angka.fold<double>(0, (acc, n) => acc + n) / angka.length;
// 3.0
Chaining — the Power of Functional Methods #
Functional methods can be chained for complex transformations without temporary variables:
List<Transaksi> transaksi = ambilSemuaTransaksi();
// One expressive pipeline
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);
// More expressive than an imperative loop:
// double total = 0;
// for (final t in transaksi) {
// if (t.jenis == JenisTransaksi.pemasukan &&
// t.tanggal.month == DateTime.now().month) {
// total += t.jumlah;
// }
// }
Other Useful Methods #
List<int> angka = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
// Condition checks
print(angka.any((n) => n > 8)); // true — there's one > 8
print(angka.every((n) => n > 0)); // true — all > 0
print(angka.contains(4)); // true
// Search
print(angka.firstWhere((n) => n > 4)); // 5 — first element > 4
print(angka.lastWhere((n) => n < 4)); // 3 — last element < 4
print(angka.firstWhereOrNull((n) => n > 100)); // null — no crash
// Extracting subsets
print(angka.take(3).toList()); // [3, 1, 4] — first 3 elements
print(angka.skip(7).toList()); // [6, 5, 3] — skip the first 7 elements
print(angka.takeWhile((n) => n < 5).toList()); // [3, 1, 4, 1] — take while < 5
print(angka.skipWhile((n) => n < 5).toList()); // [5, 9, 2, 6, 5, 3] — skip while < 5
print(angka.sublist(2, 5)); // [4, 1, 5] — slice [2, 5)
// Info
print(angka.elementAt(3)); // 1 — same as angka[3]
Sorting — Ordering It Right #
Basic Sorting #
List<int> angka = [5, 3, 1, 4, 2];
// sort() changes the original list (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 Objects #
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 by a single criterion
produk.sort((a, b) => a.harga.compareTo(b.harga)); // price ascending
produk.sort((a, b) => b.stok.compareTo(a.stok)); // stock descending
produk.sort((a, b) => a.nama.compareTo(b.nama)); // name A-Z
// Sort by several criteria (compound sort)
produk.sort((a, b) {
final byHarga = a.harga.compareTo(b.harga);
if (byHarga != 0) return byHarga; // primary: price ascending
return b.stok.compareTo(a.stok); // tiebreaker: stock descending
});
// ANTI-PATTERN: sorting a list that should be immutable
final daftar = List.unmodifiable([3, 1, 2]);
daftar.sort(); // ✗ UnsupportedError — the list can't be modified
// CORRECT: make a modifiable copy
final terurut = [...daftar]..sort(); // ✓ sort on a copy
// or
final terurut = List.of(daftar)..sort(); // ✓ List.of makes a growable copy
Creating a New Sorted List (Non-Mutating) #
List<int> original = [3, 1, 4, 1, 5];
// Way 1: spread + sort with a cascade
List<int> terurut = [...original]..sort();
print(original); // [3, 1, 4, 1, 5] — unchanged
print(terurut); // [1, 1, 3, 4, 5]
// Way 2: sorted from the collection package
import 'package:collection/collection.dart';
List<int> terurut2 = original.sorted(); // ✓ doesn't mutate original
Collection If and Collection For #
Dart supports if and for inside collection literals — the idiomatic way to build lists conditionally:
bool tampilkanBonus = true;
List<String> menu = [
'Nasi Goreng',
'Mie Goreng',
if (tampilkanBonus) 'Es Krim Gratis', // only added if 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(), // condition inside 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 #
This is one of the differences with the biggest performance impact, and it’s rarely understood:
// EAGER (List) — the entire result is computed and stored in memory at once
List<int> angka = List.generate(1_000_000, (i) => i);
List<int> hasilEager = angka
.map((n) => n * n) // creates a List of 1 million elements
.where((n) => n > 10) // creates another new List from 1 million elements
.toList(); // finally toList()
// Memory: allocated 3 times for a large list
// LAZY (Iterable) — only computed when needed
Iterable<int> hasilLazy = angka
.map((n) => n * n) // not computed yet — only a "recipe"
.where((n) => n > 10); // not computed yet — only another "recipe"
// Computed one at a time as iterated
for (final n in hasilLazy) {
// each n is only computed here — no large list allocated in memory
if (n > 1000) break; // can stop midway — saves even more
}
// When to use Iterable (lazy) vs List (eager)
// USE Iterable when:
// - The collection is very large or unbounded
// - You might stop iterating before finishing (take, firstWhere)
// - You only need to iterate once
// - Chaining many transformations
// USE List when:
// - You need random access (by index)
// - You need to iterate more than once
// - You need an accurate length before iterating
// - Handing data to an API that accepts List
Unmodifiable Lists #
For public APIs or data that must not change after being produced, wrap the list with List.unmodifiable or UnmodifiableListView:
class KatalogProduk {
final List<Produk> _produk;
KatalogProduk(List<Produk> produk) : _produk = List.of(produk);
// Return a view that can't be modified
List<Produk> get produk => List.unmodifiable(_produk);
// Or use UnmodifiableListView from dart:collection — more memory efficient
// (doesn't make a copy, just a wrapper)
List<Produk> get produkView => UnmodifiableListView(_produk);
}
// Callers can't modify the internal list
final katalog = KatalogProduk([p1, p2, p3]);
katalog.produk.add(p4); // ✗ UnsupportedError
katalog.produk[0] = p4; // ✗ UnsupportedError
// ANTI-PATTERN: exposing the internal list directly
class ProdukRepository {
final List<Produk> _data = [];
List<Produk> get semua => _data; // ✗ callers can modify _data directly!
}
// CORRECT: return an unmodifiable view or copy
class ProdukRepository {
final List<Produk> _data = [];
List<Produk> get semua => List.unmodifiable(_data); // ✓
// or: UnmodifiableListView(_data) for zero-copy
}
Combining and Flattening Lists #
// Combining several lists — the right way
List<int> a = [1, 2, 3];
List<int> b = [4, 5, 6];
List<int> c = [7, 8, 9];
// Spread — most idiomatic for a known number of lists
List<int> gabung = [...a, ...b, ...c]; // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// expand — for flattening List<List<T>> into 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]
// Combining many lists dynamically
List<List<int>> semuaList = [a, b, c];
List<int> gabungDinamis = semuaList.expand((l) => l).toList();
// ANTI-PATTERN: using += or + in a loop — O(n²) memory
List<int> hasil = [];
for (final subList in semuaList) {
hasil = hasil + subList; // ✗ creates a new List every iteration
}
// ANTI-PATTERN: addAll in a loop — still O(n) but verbose
List<int> hasil = [];
for (final subList in semuaList) {
hasil.addAll(subList); // possible, but expand is more idiomatic
}
// CORRECT: expand or spread
List<int> hasil = semuaList.expand((l) => l).toList(); // ✓ O(n), most idiomatic
Grouping and Partitioning #
For grouping operations often needed in real applications, use the collection package:
import 'package:collection/collection.dart';
List<Transaksi> transaksi = [...];
// groupBy — group by a criterion
Map<String, List<Transaksi>> perKategori =
groupBy(transaksi, (t) => t.kategori);
// {'makanan': [...], 'transport': [...], 'belanja': [...]}
// partition — split a list into two based on a condition
final (berhasil, gagal) = transaksi.partition((t) => t.sukses);
// berhasil: all successful, gagal: all unsuccessful
// If you don't want to add a package, a manual implementation:
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;
}
List Anti-Patterns to Avoid #
Modifying a List While Iterating #
List<int> angka = [1, 2, 3, 4, 5];
// ANTI-PATTERN: modifying during for-in — ConcurrentModificationError
for (final n in angka) {
if (n.isEven) angka.remove(n); // ✗ runtime error
}
// CORRECT: filter into a new list (most idiomatic)
angka = angka.where((n) => n.isOdd).toList(); // ✓
// CORRECT: iterate backward for in-place modification
for (int i = angka.length - 1; i >= 0; i--) {
if (angka[i].isEven) angka.removeAt(i); // ✓ safe because iterating from the back
}
Checking Duplicates with contains in a Loop
#
// ANTI-PATTERN: List.contains in a loop — O(n²)
List<String> unik = [];
for (final item in daftar) {
if (!unik.contains(item)) { // ✗ O(n) per iteration — total O(n²)
unik.add(item);
}
}
// CORRECT: use a Set for deduplication — O(n)
List<String> unik = daftar.toSet().toList(); // ✓ O(n)
// Note: order may not be preserved — use LinkedHashSet if needed
String Concatenation in a Loop #
// ANTI-PATTERN: String concatenation with join in a large list
List<String> kata = List.generate(10000, (i) => 'kata$i');
String hasil = '';
for (final k in kata) {
hasil += '$k '; // ✗ creates a new String every iteration — O(n²) memory
}
// CORRECT: use join (O(n))
String hasil = kata.join(' '); // ✓ most efficient
// Or StringBuffer if you need more control
final buffer = StringBuffer();
for (final k in kata) {
buffer.write(k);
buffer.write(' ');
}
String hasil = buffer.toString(); // ✓ O(n)
Summary #
- The three most useful ways to create a List: literal
[...],List.generate()for patterned lists, andList.from()/List.of()for converting from an Iterable.- Fixed-length vs growable:
List.filled()produces a fixed-length list — its size can’t change. The literal[]andList.empty(growable: true)produce growable lists.const Listis fully immutable: both size and values can’t be changed. Unlikefinal List, where only the reference can’t be replaced but the contents still can.- Functional methods work lazily (
map,where,take,skip) — they produce an unevaluatedIterable. Call.toList()to get an evaluatedList.- Method chaining creates expressive transformation pipelines without temporary variables — more readable than imperative loops for filter, transform, and aggregation operations.
reducethrows on an empty list — usefoldwith an initial value for safety, especially when working with data that could be empty.- Collection if and collection for let you build lists conditionally directly inside literals — a very expressive Dart idiom.
- Spread
...and...?are the best way to combine a known number of lists. For dynamically combining lists, useexpand.- Don’t expose internal lists directly — return
List.unmodifiable()orUnmodifiableListViewso callers can’t corrupt internal state.- Use a
Setfor deduplication, notList.containsin a loop.containson a List is O(n), making a dedup loop O(n²) — very slow for large data.