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, and List.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 [] and List.empty(growable: true) produce growable lists.
  • const List is fully immutable: both size and values can’t be changed. Unlike final 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 unevaluated Iterable. Call .toList() to get an evaluated List.
  • Method chaining creates expressive transformation pipelines without temporary variables — more readable than imperative loops for filter, transform, and aggregation operations.
  • reduce throws on an empty list — use fold with 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, use expand.
  • Don’t expose internal lists directly — return List.unmodifiable() or UnmodifiableListView so callers can’t corrupt internal state.
  • Use a Set for deduplication, not List.contains in a loop. contains on a List is O(n), making a dedup loop O(n²) — very slow for large data.

← Previous: Exceptions   Next: Map →

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