Chương 06 · Collections & Iterables

Collections & Iterables

Collection là tool dùng mỗi ngày. Higher-order method (map/where/fold) là pattern functional cốt lõi — Flutter UI build từ list widget (children: items.map(...).toList()), state class lưu collection (List<Todo>). Collection-if/for là cú pháp rất Flutter-friendly để xây widget tree có điều kiện.

Độ dài: ~950 dòng Bài tập: 5 Quiz: 8 Prerequisites: Ch 2, 3
🎯 Mục tiêu chương
  • Sử dụng thành thạo List, Set, Map với generic type.
  • Phân biệt growable / fixed / unmodifiable list.
  • Hiểu Iterablelazy — chỉ compute khi consume.
  • Dùng higher-order method: map, where, fold, reduce, expand, take, skip, any, every, firstWhere.
  • Dùng spread operator ......?.
  • Dùng collection-if / collection-for: [if (cond) x, for (var i in xs) i].
  • Hiểu khi nào tách Iterable → List bằng .toList().
  • Sort, group by, unique.

1. 3 collection cơ bản

// List — sequence ordered, allow duplicate
List<int> numbers = [1, 2, 3, 2];   // length 4

// Set — unique, unordered nominal
Set<int> uniqs = {1, 2, 3, 2};      // {1, 2, 3}

// Map — key-value
Map<String, int> ages = {'Việt': 25, 'Anh': 30};
ListSetMap
OrderedCó (insertion)Không (default), có với LinkedHashSetCó (insertion) với LinkedHashMap default
UniqueKhôngKey unique
LookupO(n) containsO(1) avg containsO(1) avg by key
Literal[]{} (có element){} (có :)
Use caseSequence, order matterDedup, fast containsLookup by key

2. List — literal & constructor

// Literal — type inferred
final a = [1, 2, 3];               // List<int>
final b = <String>[];                // Empty List<String> growable

// Constructor
final c = List.filled(5, 0);          // [0,0,0,0,0]
final d = List.generate(5, (i) => i*2); // [0,2,4,6,8]
final e = List.of([1, 2, 3]);          // copy
final f = List.from([1, 2, 3]);        // copy (lenient type)

Growable vs fixed-length

// Growable (default)
final g = <int>[];
g.add(1);  // OK

// Fixed
final f = List.filled(5, 0, growable: false);
f[0] = 99;       // OK — assign
// f.add(1);     // ❌ UnsupportedError

Method thông dụng

final xs = [1, 2, 3];
xs.add(4);                  // [1,2,3,4]
xs.insert(0, 0);              // [0,1,2,3,4]
xs.remove(3);               // remove value 3 → [0,1,2,4]
xs.removeAt(0);             // remove index 0 → [1,2,4]
xs.length;                  // 3
xs.indexOf(2);              // 1
xs.contains(99);            // false
xs.reversed.toList();        // [4,2,1]
xs.sublist(1, 3);            // [2,4]
xs.sort();                  // in-place mutate

3. Set

final a = {1, 2, 3};
final b = {2, 3, 4};

a.union(b);              // {1, 2, 3, 4}
a.intersection(b);       // {2, 3}
a.difference(b);         // {1}

a.contains(1);          // true — O(1) avg, nhanh hơn List.contains O(n)

// Empty set — phải dùng constructor vì {} mặc định là Map
final empty1 = <int>{};         // Set rỗng
final empty2 = <int>[]                 .toSet();
// final empty3 = {};     // ❌ Đây là Map rỗng

4. Map

final ages = {'Việt': 25, 'Anh': 30};

print(ages['Việt']);          // 25
print(ages['X']);              // null (key không tồn tại)
ages['Phương'] = 22;            // add or update
ages.remove('Anh');
ages.containsKey('Việt');        // true
ages.containsValue(25);          // true
ages.keys;                        // Iterable<String>
ages.values;                      // Iterable<int>
ages.entries;                     // Iterable<MapEntry<String, int>>

for (final entry in ages.entries) {
  print('${entry.key}: ${entry.value}');
}

// putIfAbsent — pattern thường gặp
final counts = <String, int>{};
for (final word in ['a', 'b', 'a', 'c', 'a']) {
  counts[word] = (counts[word] ?? 0) + 1;
}
print(counts);  // {a: 3, b: 1, c: 1}

5. Iterable — lazy by design

Iterable là parent của List, Set (cùng cả Stream, sẽ học chương 7). Method map, where, take trả về Iterablekhông compute đến khi consume.

final result = [1, 2, 3].map((x) {
  print('mapping $x');
  return x * 2;
});

print('created');              // Không in mapping nào!

print(result.toList());          // Bây giờ mới in:
// mapping 1
// mapping 2
// mapping 3
// [2, 4, 6]
🧠 Lazy != bad — chain dài không cost gấp

Chain dài như xs.where(p).map(f).take(5) KHÔNG iterate 3 lần. Mỗi element đi qua pipe một lần — lazy đảm bảo. Chỉ khi .toList() hoặc for-in consume, pipe mới chạy.

So với JS Array: JS .map().filter().take(5) tạo array trung gian mỗi step — eager. Dart lazy hiệu quả hơn.

6. Higher-order methods

map — transform

final nums = [1, 2, 3];
final doubled = nums.map((x) => x * 2).toList();   // [2, 4, 6]
final labels = nums.map((x) => '#$x').toList();   // ['#1', '#2', '#3']

where — filter

final evens = [1, 2, 3, 4, 5].where((x) => x % 2 == 0);
print(evens.toList());  // [2, 4]

foldreduce

final xs = [1, 2, 3, 4];

// fold — có initial value, type kết quả có thể khác element type
final sum = xs.fold<int>(0, (acc, x) => acc + x);
print(sum);  // 10

final labels = xs.fold<String>('', (acc, x) => '$acc-$x');
print(labels);  // '-1-2-3-4'

// reduce — không initial, type acc phải == element type
final sum2 = xs.reduce((a, b) => a + b);
print(sum2);  // 10

// reduce throw nếu Iterable rỗng — fold an toàn hơn
// [].reduce((a, b) => a + b);  // Bad state: No element

Khác

final xs = [1, 2, 3, 4, 5];

xs.expand((x) => [x, x*10]).toList();   // [1,10,2,20,3,30,4,40,5,50] — flatMap
xs.take(3).toList();                    // [1,2,3]
xs.skip(2).toList();                    // [3,4,5]
xs.any((x) => x > 4);                  // true
xs.every((x) => x > 0);                // true
xs.firstWhere((x) => x > 3);           // 4 (throw nếu không có)
xs.firstWhere((x) => x > 99, orElse: () => -1);  // -1
🔀 So với JavaScript Array
JSDart
arr.map(f)xs.map(f).toList() hoặc .toSet()
arr.filter(p)xs.where(p).toList()
arr.reduce(...)xs.fold(initial, ...) hoặc xs.reduce(...)
arr.flatMap(f)xs.expand(f).toList()
arr.find(p)xs.firstWhere(p, orElse: () => null)
arr.some(p)xs.any(p)
arr.every(p)xs.every(p)
arr.sort() (mutate)xs.sort() (mutate)

7. Spread operator

final a = [1, 2];
final b = [3, 4];

final merged = [...a, ...b, 5];        // [1,2,3,4,5]

// Spread null-safe
List<int>? maybe;
final safe = [0, ...?maybe, 99];        // [0, 99]

8. Collection-if / collection-for

Cú pháp cực Flutter-friendly — xây List/Set/Map có conditional inline.

final isAdmin = true;
final menu = [
  'Home',
  'Profile',
  if (isAdmin) 'Admin Panel',
  'Settings',
];
print(menu);
// [Home, Profile, Admin Panel, Settings]

// Collection-for
final doubled = [for (var x in [1, 2, 3]) x * 2];
print(doubled);  // [2, 4, 6]

// Kết hợp
final grid = [
  for (var row = 0; row < 3; row++)
    for (var col = 0; col < 3; col++)
      if (row + col == 2) '($row,$col)',
];
print(grid);  // [(0,2), (1,1), (2,0)]
💡 Flutter widget tree dùng pattern này
Column(
  children: [
    const Text('Welcome'),
    if (user.isPremium) const PremiumBadge(),
    for (final item in items) ListTile(title: Text(item.title)),
  ],
)

9. Sort & comparator

final xs = [3, 1, 4, 1, 5, 9, 2];
xs.sort();                // In-place: [1, 1, 2, 3, 4, 5, 9]

// Comparator custom
xs.sort((a, b) => b.compareTo(a));   // descending

// Sort object — bằng compareTo
class User { final String name; final int age; User(this.name, this.age); }

final users = [User('A', 30), User('B', 25), User('C', 30)];

// Sort theo age tăng, nếu cùng age thì theo tên A-Z
users.sort((a, b) {
  final byAge = a.age.compareTo(b.age);
  if (byAge != 0) return byAge;
  return a.name.compareTo(b.name);
});
🔥 sort mutate, không trả new

Khác Kotlin .sorted() trả list mới — Dart sort mutate in-place, return void. Muốn không mutate: [...xs]..sort() (copy trước).

10. Convert giữa types

final xs = [1, 2, 2, 3];
final set = xs.toSet();              // {1, 2, 3}

final entries = [MapEntry('a', 1), MapEntry('b', 2)];
final m = Map.fromEntries(entries);  // {a: 1, b: 2}

// Map.fromIterable — flexible
final nameAges = ['Việt:25', 'Anh:30'];
final map = Map.fromIterable(
  nameAges,
  key: (e) => e.split(':')[0],
  value: (e) => int.parse(e.split(':')[1]),
);
print(map);  // {Việt: 25, Anh: 30}

11. Unmodifiable view

final xs = [1, 2, 3];
final view = List.unmodifiable(xs);
// view.add(4);   // UnsupportedError

// Note: thay đổi xs vẫn ảnh hưởng đến view (cùng underlying data)
xs.add(99);
print(view);   // [1, 2, 3, 99]

Để hoàn toàn isolate: List.unmodifiable([...xs]) — copy + lock.

12. const collection

const a = [1, 2, 3];           // const list — immutable + canonical
// a.add(4);   // ❌ Unsupported

const b = [1, 2, 3];
print(identical(a, b));  // true — canonical

// var với const literal — vẫn canonical
var c = const [1, 2, 3];
print(identical(a, c));  // true

13. Performance note

OperationListSetMap
containsO(n)O(1) avgO(1) avg by key
addO(1) amortized endO(1) avgO(1) avg
insert at frontO(n)
removeAt(0)O(n)
sortO(n log n)
iterateO(n)O(n)O(n)
💡 Khi nào dùng Set thay List?

Khi cần (a) uniqueness, hoặc (b) contains nhanh. Vd: blacklist 1000 user ID — List.contains O(n), Set.contains O(1). Convert one-time: final blocked = ids.toSet();.

14. Bài tập

Top 3 premium HN user

Cho List<User> với field age, city, premium. Viết 1 expression chain lấy email của top 3 premium user theo age giảm dần, ở city 'HN'.

💡 Gợi ý đáp án
final emails = users
    .where((u) => u.premium && u.city == 'HN')
    .toList()
    ..sort((a, b) => b.age.compareTo(a.age));

final result = emails.take(3).map((u) => u.email).toList();

Lưu ý: phải .toList() trước ..sort()where trả Iterable lazy không có method sort. .. là cascade — trả về list sau sort.

Tổng giỏ hàng với fold

Refactor for-loop tính tổng giá trị giỏ hàng (List of Item với field price, qty) thành .fold.

💡 Gợi ý đáp án
// Cách for-loop
int total = 0;
for (final item in cart) {
  total += item.price * item.qty;
}

// Cách fold
final total = cart.fold<int>(0, (acc, i) => acc + i.price * i.qty);

Group books by author

Cho List<Book> với field author, title. Group thành Map<String, List<Book>>.

💡 Gợi ý đáp án
// Manual với fold
final grouped = books.fold<Map<String, List<Book>>>({}, (acc, book) {
  acc.putIfAbsent(book.author, () => []).add(book);
  return acc;
});

// Hoặc với for-loop trực tiếp
final grouped = <String, List<Book>>{};
for (final book in books) {
  grouped.putIfAbsent(book.author, () => []).add(book);
}

Package collection có helper groupBy sẵn. Tự implement giúp hiểu logic.

Build menu theo role

Build menu items cho 3 role user khác nhau (guest / user / admin) dùng collection-if. So sánh với cách viết if/else thông thường — đếm số dòng.

💡 Gợi ý đáp án
List<String> buildMenu(String role) => [
  'Home',
  if (role != 'guest') 'Profile',
  if (role == 'user' || role == 'admin') 'Settings',
  if (role == 'admin') 'Admin Panel',
  if (role == 'admin') 'User Management',
  'Help',
];

print(buildMenu('guest'));   // [Home, Help]
print(buildMenu('user'));    // [Home, Profile, Settings, Help]
print(buildMenu('admin'));   // tất cả

Cách if/else cần var + push: dài 2-3x dòng. Collection-if đẹp hơn.

Lazy prime generator

Tạo function Iterable<int> primes() trả về vô hạn prime — dùng generator (sync*). Demo primes().take(10).toList() không loop vô hạn.

💡 Gợi ý đáp án
Iterable<int> primes() sync* {
  var n = 2;
  while (true) {
    if (isPrime(n)) yield n;
    n++;
  }
}

bool isPrime(int n) {
  if (n < 2) return false;
  for (var i = 2; i * i <= n; i++) {
    if (n % i == 0) return false;
  }
  return true;
}

void main() {
  print(primes().take(10).toList());
  // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
}

sync* + yield là generator. take(10) stop generator sau 10 giá trị — không phải loop vô hạn.

15. Quiz

Q1

<int>[].runtimeType ≈ gì?

Xem đáp án

Đáp án: List<int> (growable). Cụ thể là JSArray<int> ở web hoặc _GrowableList<int> ở native — internal.

Q2

{1, 2, 3} là Set hay Map?

Xem đáp án

Đáp án: Set. Analyzer infer qua nội dung — không có : → set. {1: 'a'} là Map.

Q3

{} rỗng là Set hay Map?

Xem đáp án

Đáp án: Map. Không có context infer. Set rỗng phải <int>{} hoặc <String>{}.

Q4

[1,2,3].map((x) => x * 2) trả type gì?

Xem đáp án

Đáp án: Iterable<int>, không phải List<int>. Lazy. Cần .toList() để materialize.

Q5

xs.sort() mutate xs hay trả mới?

Xem đáp án

Đáp án: Mutate in-place. Return void. Khác Kotlin .sorted() (immutable). Muốn không mutate: [...xs]..sort().

Q6

Cách tìm element đầu thỏa điều kiện hoặc null nếu không có?

Xem đáp án

Đáp án: xs.firstWhere(pred, orElse: () => defaultValue). Hoặc dùng firstWhereOrNull từ package collection.

Q7

Spread [...xs, ...?maybeYs] — khi maybeYs null, kết quả?

Xem đáp án

Đáp án: Chỉ chứa elements của xs. ...? null-safe skip. ... thường sẽ throw nếu null.

Q8

Map.fromEntries([MapEntry('a', 1), MapEntry('b', 2)]) ≈ gì?

Xem đáp án

Đáp án: {'a': 1, 'b': 2}. Pattern thường dùng khi build map từ list pair.

16. Tổng kết

  • ✅ 3 collection: List (ordered), Set (unique), Map (key-value).
  • ✅ Iterable lazy — compute on consume.
  • ✅ Higher-order: map, where, fold, reduce, expand, take, skip, any, every, firstWhere.
  • ✅ Spread ... + null-safe ...?.
  • ✅ Collection-if / collection-for — Flutter-friendly.
  • ✅ Sort in-place + comparator custom.
  • const collection canonical.
  • ✅ Performance: List contains O(n), Set/Map O(1) avg.

17. Kết nối

  • Chương 7 — Stream: Stream là Iterable async.
  • Chương 8: Generic List<T>, collection pattern matching.
  • Flutter chương 3 — Layout: children: <Widget>[...] với collection-if.
  • Flutter chương 9+: state list entity (List<Todo>, etc).