Chương 08 · Generics, Extension, Patterns Dart 3

Generics, Extension, Patterns

Đây là chương "Dart hiện đại" — phần lớn pattern code mới ở Flutter packages 2024+ dùng các feature này. Records thay thế phần lớn use case "DTO class". Pattern matching làm code state-handling sạch hơn nhiều. Generic là bắt buộc ở mọi List<T>, Stream<T>, Bloc<E, S>.

Sau chương này, bạn có thể đọc 80% code Dart/Flutter modern. Chương kết Dart.

Độ dài: ~1040 dòng Bài tập: 5 Quiz: 8 Prerequisites: Tất cả chương 1-7
🎯 Mục tiêu chương
  • Khai báo generic class Box<T> và generic method T first<T>(List<T> xs).
  • Hiểu bounded generic T extends Comparable.
  • Viết extension method để thêm function cho built-in type.
  • Hiểu extension types (Dart 3.3+) như zero-cost wrapper.
  • Sử dụng records (int, String) và named records ({int x, int y}).
  • Dùng patterns: destructuring, switch with pattern, if-case.
  • Kết hợp sealed class + exhaustive switch.
  • Hiểu khi nào dùng record vs class.

1. Generic class

class Box<T> {
  final T value;
  Box(this.value);

  T unwrap() => value;
}

void main() {
  final intBox = Box<int>(42);
  final strBox = Box<String>('hello');

  int i = intBox.unwrap();        // type safe
  String s = strBox.unwrap();
  // String x = intBox.unwrap();  // ❌ Lỗi compile
}

Use case generic class:

  • Container: List<T>, Map<K, V>, Stream<T>.
  • Repository: Repository<User>, Repository<Post>.
  • Response wrapper: ApiResponse<T>.
  • Cache: Cache<K, V>.

2. Generic method

T first<T>(List<T> xs) => xs.first;

void main() {
  int a = first([1, 2, 3]);       // inferred T = int
  String b = first(['a', 'b']);    // inferred T = String

  // Explicit
  var c = first<int>([1, 2]);
}

// Generic method khác generic class — chỉ method có type param
class Util {
  static T first<T>(List<T> xs) => xs.first;
  static List<R> mapAll<T, R>(List<T> xs, R Function(T) f) =>
      xs.map(f).toList();
}

3. Bounded generic

// T phải implement Comparable<T>
T max<T extends Comparable<T>>(T a, T b) {
  return a.compareTo(b) > 0 ? a : b;
}

print(max(5, 3));            // 5
print(max('apple', 'banana'));   // banana
// max(true, false);   // ❌ bool không extends Comparable

4. Extension method

Thêm method cho type sẵn có — kể cả built-in int, String, hoặc class third-party.

extension StringX on String {
  String get reverse => split('').reversed.join();
  bool get isPalindrome => this == reverse;
  String repeat(int n) => List.filled(n, this).join();
}

extension IntX on int {
  String toVnd() => '$this VND';
  Duration get seconds => Duration(seconds: this);
  Duration get minutes => Duration(minutes: this);
}

void main() {
  print('racecar'.isPalindrome);     // true
  print('ab'.repeat(3));               // ababab
  print(1000.toVnd());                // 1000 VND

  await Future.delayed(2.seconds);     // đẹp như Kotlin/Swift
}
💡 Extension không override

Extension method không override method gốc — nếu trùng tên, instance method gốc thắng:

extension on String {
  String toLowerCase() => 'extension';
}
print('HELLO'.toLowerCase());  // 'hello' — method gốc thắng

5. Extension types (Dart 3.3+)

Khác extension method. Extension type tạo type mới chỉ tồn tại tại compile-time — runtime vẫn là type gốc. Zero-cost wrapper.

extension type Email(String value) {
  bool get isValid => value.contains('@');
  String get domain => value.split('@').last;
}

void main() {
  final e = Email('viet@example.com');
  print(e.isValid);      // true
  print(e.domain);       // example.com

  // Compile-time type khác String
  String s = 'plain';
  // Email e2 = s;     // ❌ String không assign vào Email
  // String s2 = e;    // ❌ Email không assign vào String

  // Runtime — vẫn là String
  print(e.runtimeType);  // String
  print(identical(e as Object, 'viet@example.com' as Object));   // true
}

Use case extension type:

  • Domain primitive: UserId, Email, Url — không nhầm với String thường.
  • Validation enforce: chỉ tạo qua factory, đảm bảo valid.
  • Performance: không boxing như class wrapper truyền thống.
🔀 Extension method vs Extension type
Extension methodExtension type
Tạo type mới?KhôngCó (compile-time)
Runtime cost00 (boxing-free)
Use caseThêm helper methodDomain type-safety
Type checkString vẫn là StringEmail != String compile-time

6. Records

Records (Dart 3) = anonymous tuple/struct. Structural equality. Lightweight alternative cho class khi chỉ cần "túi data".

Positional

final r = (1, 'a');                // type: (int, String)
print(r.$1);                       // 1
print(r.$2);                       // 'a'

// Mixed type OK
final mix = (42, 'hi', true);    // (int, String, bool)

Named

final user = (name: 'Việt', age: 25);
print(user.name);                // 'Việt'
print(user.age);                 // 25

// Type: ({String name, int age})
({String name, int age}) makeUser() => (name: 'Việt', age: 25);

// Mix positional + named
final mixed = (1, 2, label: 'x');
print(mixed.$1);  // 1
print(mixed.label);  // 'x'

Structural equality

final a = (1, 'a');
final b = (1, 'a');
print(a == b);       // true — so theo nội dung, không identity

// Khác class
final p1 = Point(1, 2);
final p2 = Point(1, 2);
print(p1 == p2);    // false (trừ khi override ==)

Record vs class — khi nào dùng?

RecordClass
EqualityStructural (auto)Identity (cần override ==)
MutableKhôngTuỳ field
MethodKhông có (chỉ field)
Identity stableKhông
Use caseReturn multiple value, intermediate stateDomain entity, behavior

7. Patterns — destructuring

Destructure record

(int, String) parseLine(String line) {
  final parts = line.split(':');
  return (int.parse(parts[0]), parts[1]);
}

// Destructure
final (id, name) = parseLine('1:Việt');
print('id=$id, name=$name');   // id=1, name=Việt

// Destructure named
final (name: n, age: a) = (name: 'Việt', age: 25);
print('$n $a');

List pattern

final xs = [1, 2, 3, 4, 5];
final [first, second, ...rest] = xs;
print(first);   // 1
print(second);  // 2
print(rest);    // [3, 4, 5]

Map pattern

final json = {'name': 'Việt', 'age': 25};
final {'name': name, 'age': age} = json;
print('$name $age');

8. Pattern trong switch

sealed class Shape {}
class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}
class Square extends Shape {
  final double side;
  Square(this.side);
}

double area(Shape s) => switch (s) {
  Circle(:var radius) => 3.14 * radius * radius,
  Square(:var side) => side * side,
};

// Sealed + exhaustive — không cần default!

9. Pattern + when guard

String describePoint((int, int) p) => switch (p) {
  (0, 0) => 'origin',
  (var x, 0) => 'on x-axis: $x',
  (0, var y) => 'on y-axis: $y',
  (var x, var y) when x == y => 'on diagonal at $x',
  _ => 'normal point',
};

print(describePoint((0, 0)));    // origin
print(describePoint((5, 0)));    // on x-axis: 5
print(describePoint((3, 3)));    // on diagonal at 3

10. if-case

final json = {'name': 'Việt', 'age': 25};

if (json case {'name': String name, 'age': int age}) {
  print('Valid user: $name $age');
} else {
  print('Invalid format');
}

// Combine với pattern type check
final input = '42';
if (int.tryParse(input) case int n when n > 0) {
  print('positive: $n');
}

11. Sealed + exhaustive switch — pattern chuẩn

sealed class ApiState<T> {
  const ApiState();
}

class Idle<T> extends ApiState<T> {
  const Idle();
}

class Loading<T> extends ApiState<T> {
  const Loading();
}

class Success<T> extends ApiState<T> {
  final T data;
  const Success(this.data);
}

class Failure<T> extends ApiState<T> {
  final String message;
  const Failure(this.message);
}

String describe(ApiState<User> state) => switch (state) {
  Idle() => 'chưa load',
  Loading() => 'đang load...',
  Success(:var data) => 'load xong: ${data.name}',
  Failure(:var message) => 'lỗi: $message',
};
// Compiler verify đủ 4 case — không cần default
🧠 Pattern này thay thế chain if/else

Trước Dart 3 (hoặc Java/JS), bạn viết:

if (state is Loading) ...
else if (state is Success) print(state.data);
else if (state is Failure) ...
else { // Idle... }

Pattern matching ngắn hơn + compiler enforce exhaustive. Forget case → compile fail. Code maintain dễ hơn nhiều.

12. Object pattern

class Point {
  final int x, y;
  Point(this.x, this.y);
}

String describe(Point p) => switch (p) {
  Point(x: 0, y: 0) => 'origin',
  Point(:var x, :var y) when x == y => 'diagonal: $x',
  Point(:var x, :var y) => '($x, $y)',
};

Point(:var x, :var y) = Point(x: var x, y: var y) shorthand. : bind tự động tên field thành biến cùng tên.

13. Khi nào dùng feature nào?

🎯 Decision guide
  • Generic class: khi container cần work với nhiều type.
  • Generic method: khi function nhận arg + return có type relation.
  • Bounded generic: khi cần gọi method của type bound (vd Comparable).
  • Extension method: thêm helper cho built-in / third-party type, fluent API.
  • Extension type: domain primitive — Email, UserId, Url khác String.
  • Records: return multiple value, intermediate state, "anonymous DTO".
  • Class: domain entity có behavior, identity, lifecycle.
  • Sealed + pattern match: state machine, Result type, AST node.
  • If-case: validate single shape, không cần multiple case.

14. Bài tập

Generic Stack<T>

Implement class Stack<T> với push, pop, peek, isEmpty. Test với Stack<int>Stack<String>.

💡 Gợi ý đáp án
class Stack<T> {
  final _items = <T>[];

  void push(T item) => _items.add(item);
  T pop() => _items.removeLast();
  T peek() => _items.last;
  bool get isEmpty => _items.isEmpty;
  int get length => _items.length;
}

void main() {
  final ints = Stack<int>()..push(1)..push(2)..push(3);
  print(ints.pop());     // 3
  print(ints.peek());    // 2

  final strs = Stack<String>()..push('a')..push('b');
  print(strs.pop());     // 'b'
}

DateTime extensions

Viết extension DateTimeX on DateTime với isToday, isYesterday, relative (vd "3 hours ago").

💡 Gợi ý đáp án
extension DateTimeX on DateTime {
  bool get isToday {
    final now = DateTime.now();
    return year == now.year && month == now.month && day == now.day;
  }

  bool get isYesterday {
    final yesterday = DateTime.now().subtract(Duration(days: 1));
    return year == yesterday.year && month == yesterday.month && day == yesterday.day;
  }

  String get relative {
    final diff = DateTime.now().difference(this);
    if (diff.inMinutes < 1) return 'vừa xong';
    if (diff.inHours < 1) return '${diff.inMinutes} phút trước';
    if (diff.inDays < 1) return '${diff.inHours} giờ trước';
    if (diff.inDays < 30) return '${diff.inDays} ngày trước';
    return toIso8601String();
  }
}

Map → record refactor

Refactor function trả Map<String, dynamic> chứa 3 field thành function trả record (String name, int age, bool premium). So sánh code caller.

💡 Gợi ý đáp án
// Trước (dynamic)
Map<String, dynamic> getUser() {
  return {'name': 'Việt', 'age': 25, 'premium': true};
}
final u = getUser();
print(u['name']);  // dynamic, không type-safe

// Sau (record)
({String name, int age, bool premium}) getUser() {
  return (name: 'Việt', age: 25, premium: true);
}
final u = getUser();
print(u.name);  // String — type-safe
print(u.age);

// Destructure
final (name: n, age: a, premium: p) = getUser();

Sealed ApiState<T>

Implement sealed class ApiState<T> với 4 case: Idle, Loading, Success(T data), Error(String message). Viết String describe(ApiState state) dùng switch exhaustive.

💡 Gợi ý đáp án
sealed class ApiState<T> { const ApiState(); }
class Idle<T> extends ApiState<T> { const Idle(); }
class Loading<T> extends ApiState<T> { const Loading(); }
class Success<T> extends ApiState<T> {
  final T data;
  const Success(this.data);
}
class Error<T> extends ApiState<T> {
  final String message;
  const Error(this.message);
}

String describe(ApiState<String> s) => switch (s) {
  Idle() => 'chưa load',
  Loading() => 'đang load',
  Success(:var data) => 'kết quả: $data',
  Error(:var message) => 'lỗi: $message',
};

void main() {
  print(describe(Idle()));
  print(describe(Loading()));
  print(describe(Success('hello')));
  print(describe(Error('network')));
}

Validate JSON với if-case

Validate JSON shape với if-case: nhận Map<String, dynamic>, return User? nếu hợp lệ shape {name: String, age: int}. Không dùng if/else chain.

💡 Gợi ý đáp án
class User { final String name; final int age; User(this.name, this.age); }

User? parseUser(Map<String, dynamic> json) {
  if (json case {'name': String name, 'age': int age}) {
    return User(name, age);
  }
  return null;
}

void main() {
  print(parseUser({'name': 'Việt', 'age': 25}));         // User
  print(parseUser({'name': 'Việt'}));                     // null
  print(parseUser({'name': 'Việt', 'age': 'thirty'}));    // null (wrong type)
}

Single line check + bind 2 variable cùng lúc. Sạch hơn nhiều so với chain check.

15. Quiz

Q1

T first<T>(List<T> xs) — gọi first([1, 2, 3]) type là gì?

Xem đáp án

Đáp án: int. Inferred từ argument List<int>.

Q2

Extension thêm method toVnd cho int — instance method gốc trùng tên thì sao?

Xem đáp án

Đáp án: Method gốc thắng. Extension không override built-in method. Đặt tên unique để tránh conflict.

Q3

Extension type Email runtime là gì?

Xem đáp án

Đáp án: String (boxing-free). Compile-time type khác String, nhưng runtime không có wrapper object — performance bằng String thường.

Q4

(1, 'a') == (1, 'a')true hay false?

Xem đáp án

Đáp án: true. Record equality structural — so theo nội dung.

Q5

({int age, String name})({String name, int age}) — same type không?

Xem đáp án

Đáp án: Có. Named record so theo tên + type, không thứ tự. Khác positional record (thứ tự matter).

Q6

Sealed class trong file A, switch ở file B không cover hết case — analyzer cảnh báo không?

Xem đáp án

Đáp án: Có. Sealed enforce exhaustive — compiler biết tất cả subclass (chỉ trong cùng library A), check switch B đủ chưa.

Q7

case Point(:var x, :var y)case Point(x: var x, y: var y)?

Xem đáp án

Đáp án: Có. :var x shorthand cho x: var x (bind field name vào biến cùng tên).

Q8

Khi nào nên dùng record thay class?

Xem đáp án

Đáp án: Data tạm thời, không cần method/identity/behavior. Đại diện: function return nhiều giá trị, intermediate state. Class khi có domain identity, behavior, lifecycle.

16. Tổng kết Dart curriculum

Bạn đã hoàn thành 8 chương Dart. Chương trình bao gồm:

  1. Hello Dart — setup, CLI, project structure.
  2. Variables & Types — type system, var/final/const, null safety basic.
  3. Functions & Control Flow — named param, closure, switch expression.
  4. Classes & Inheritance — constructor 5 loại, mixin, class modifier Dart 3.
  5. Null Safety & Error Handling — sound null safety, late, exception.
  6. Collections & Iterables — lazy Iterable, higher-order, collection-if/for.
  7. Async, Future & Stream — event loop, Future, Stream, Isolate.
  8. Generics, Extension, Patterns — generic, extension type, record, pattern matching.
🧠 Bạn đã có nền Dart vững — sang Flutter

Mọi Dart pattern Flutter dùng đã trong tay. Flutter sub-pillar sẽ áp dụng:

  • Named params + required (chương 3) → mọi widget constructor.
  • Class extends (chương 4) → StatelessWidget, StatefulWidget.
  • Stream (chương 7) → Bloc state, StreamBuilder.
  • Sealed + pattern (chương 8) → Bloc state class.
  • copyWith (chương 4) → immutable state.

17. Tiếp tục — Flutter sub-pillar

Sang Flutter sub-pillar (15 chương) để build app cross-platform với Dart. Bắt đầu từ Flutter Setup → Widget Tree → Layout → Theming → ... → Performance/Deploy.