Chương 04 · Classes & Inheritance

Classes & Inheritance

Mọi Widget Flutter là class extends StatelessWidget hoặc StatefulWidget. Mọi state management Bloc/Cubit dùng inheritance. Pattern copyWith xuất hiện ở mọi immutable state class. Class modifier Dart 3 (sealed) là nền tảng cho exhaustive pattern matching ở chương 8.

Bạn đã học OOP ở trụ cột OOP & Design Patterns (8 chương) — chương này không dạy lại OOP từ đầu, mà map kiến thức OOP sang cú pháp Dart, nhấn mạnh điểm khác biệt (implicit interface, mixin, factory constructor, class modifier).

Độ dài: ~1050 dòng Bài tập: 5 Quiz: 8 Prerequisites: Ch 2, 3 + OOP pillar
🎯 Mục tiêu chương
  • Khai báo class với field, method, constructor.
  • Sử dụng đủ 5 loại constructor: default, named, initializer list, redirecting, factory.
  • Hiểu getter/setter (computed property).
  • Phân biệt instance member vs static member.
  • Inheritance: extends, @override, super, abstract class/method.
  • Interface (implicit interface) và implements.
  • Mixin: mixin, with, on constraint.
  • Class modifier Dart 3: base, final, sealed, interface, mixin class.
  • Override toString, ==, hashCode + pattern copyWith.

1. Class anatomy

class Point {
  // Fields
  final int x;
  final int y;

  // Constructor
  Point(this.x, this.y);

  // Method
  double distanceTo(Point other) {
    final dx = x - other.x;
    final dy = y - other.y;
    return (dx * dx + dy * dy).toDouble().sqrt();
  }
}

void main() {
  final p = Point(3, 4);
  print(p.x);                       // 3
  print(p.distanceTo(Point(0, 0))); // 5
}

Naming conventions:

  • Class: PascalCase (Point, HttpClient).
  • Field, method, variable: camelCase.
  • Private: prefix _ (library-private, không class-private).
🧠 new keyword? Không cần

Dart 2+ new là optional. Idiomatic không dùng:

final p1 = new Point(3, 4);   // OK nhưng lỗi thời
final p2 = Point(3, 4);       // idiomatic Dart

2. Constructor — 5 loại

2.1 Default constructor

class Point {
  final int x;
  final int y;

  // Cách dài
  Point(int x, int y)
      : x = x,
        y = y;

  // Cách ngắn — shorthand "this.x"
  Point(this.x, this.y);
}

this.x shorthand tự gán param vào field tương ứng — tránh boilerplate.

2.2 Named constructor — nhiều cách tạo instance

class Point {
  final int x;
  final int y;

  // Default
  Point(this.x, this.y);

  // Named
  Point.origin() : x = 0, y = 0;
  Point.fromMap(Map<String, int> m) : x = m['x']!, y = m['y']!;
}

final a = Point(3, 4);
final b = Point.origin();
final c = Point.fromMap({'x': 1, 'y': 2});

2.3 Initializer list — chạy trước body

class Rectangle {
  final int width;
  final int height;
  final int area;     // final phải gán ở initializer list

  Rectangle(this.width, this.height)
      : area = width * height,
        assert(width > 0),
        assert(height > 0) {
    print('Constructed: $width × $height');
  }
}

Initializer list chạy trước body. Dùng cho:

  • Gán final field từ expression dùng param.
  • assert sanity check.
  • Gọi super(...) với arg.

2.4 Const constructor

class Point {
  final int x;
  final int y;

  const Point(this.x, this.y);   // const constructor — yêu cầu mọi field final
}

const a = Point(1, 2);
const b = Point(1, 2);
print(identical(a, b));  // true — canonicalized!

Class có const constructor → 2 instance const cùng arg dùng cùng memory. Cực kỳ quan trọng cho Flutter performance (widget không rebuild khi const).

2.5 Redirecting constructor

class Point {
  final int x, y;

  Point(this.x, this.y);
  Point.zero() : this(0, 0);              // redirect tới default
  Point.diag(int v) : this(v, v);
}

2.6 Factory constructor — không bắt buộc trả instance mới

Đây là loại constructor đặc biệt của Dart. factory không bắt buộc trả instance mới — có thể cache, return subclass, hoặc null (nullable type).

class Logger {
  static final _instances = <String, Logger>{};
  final String name;

  Logger._internal(this.name);  // private constructor

  factory Logger(String name) {
    return _instances.putIfAbsent(name, () => Logger._internal(name));
  }

  void log(String msg) => print('[$name] $msg');
}

void main() {
  final a = Logger('http');
  final b = Logger('http');
  print(identical(a, b));   // true — same instance, cache hit

  final c = Logger('db');
  print(identical(a, c));   // false — khác name, instance khác
}

Use cases factory:

  • Singleton per key (như Logger trên).
  • Parse JSON: factory User.fromJson(Map json) => User(...).
  • Return subclass: factory Shape.from(String type) => type == 'circle' ? Circle() : Square();.
  • Cache expensive object.

3. Getter / Setter — computed property

class Rectangle {
  int width, height;

  Rectangle(this.width, this.height);

  // Getter — không có ()
  int get area => width * height;
  int get perimeter => 2 * (width + height);

  // Setter
  set dimensions(List<int> xy) {
    width = xy[0];
    height = xy[1];
  }
}

final r = Rectangle(3, 4);
print(r.area);          // 12 — gọi như field, không có ()
r.dimensions = [5, 6]; // gọi setter
💡 Dart không phân biệt field access vs getter call

Từ caller, r.area trông giống r.width (field). Đây là feature: bạn có thể refactor field thành getter mà không breaking caller code.

4. Static member

class MathHelper {
  static const pi = 3.14159;

  static int square(int x) => x * x;
  static int cube(int x) => x * x * x;
}

print(MathHelper.pi);          // 3.14159
print(MathHelper.square(5));   // 25

// Không gọi từ instance
final h = MathHelper();
// h.square(5);   // ❌ Lỗi

5. Private — prefix _

Dart không có private keyword. Convention: prefix _ = private.

// File: user.dart
class User {
  String name;            // public
  String _passwordHash;   // private — chỉ accessible trong cùng file/library

  User(this.name, this._passwordHash);
}

// File: main.dart
import 'user.dart';

void main() {
  final u = User('Việt', 'abc123');
  print(u.name);            // OK
  // print(u._passwordHash); // ❌ Lỗi — private khác file
}
⚠️ Private = library scope, không phải class scope

Khác Java/C# (private = class scope). Dart private = library scope — mọi file trong cùng library access được. Đa số case: 1 file = 1 library, nên ≈ file scope.

6. Inheritance — extends

class Animal {
  final String name;
  Animal(this.name);

  void speak() => print('$name làm âm thanh chung');
}

class Dog extends Animal {
  Dog(String name) : super(name);

  @override
  void speak() => print('$name gâu gâu');
}

void main() {
  Animal a = Dog('Mực');
  a.speak();           // Mực gâu gâu (polymorphism)
}

Quy tắc:

  • Dart single inheritance — chỉ 1 parent. Multi-inheritance qua mixin.
  • @override annotation — không bắt buộc nhưng analyzer recommend (catch typo).
  • super(args) trong initializer list gọi constructor parent.
  • Mọi class implicit extends Object.

7. Abstract class & method

abstract class Shape {
  double area();           // abstract method — không body
  double perimeter();

  // Concrete method — có body, mọi subclass thừa kế
  void describe() {
    print('Area: ${area()}, Perimeter: ${perimeter()}');
  }
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double area() => 3.14 * radius * radius;

  @override
  double perimeter() => 2 * 3.14 * radius;
}

// Shape s = Shape();   // ❌ Lỗi — không instantiate abstract
Shape s = Circle(5);
s.describe();

8. Implicit interface — implements

Mọi class Dart là interface ngầm của chính nó. Không cần định nghĩa interface riêng.

class Animal {
  void eat() => print('ăn');
  void sleep() => print('ngủ');
}

// Implements — chỉ commit signature, KHÔNG kế thừa code
class Robot implements Animal {
  @override
  void eat() => print('nạp pin');

  @override
  void sleep() => print('standby');
}

Animal r = Robot();
r.eat();   // nạp pin
🔀 extends vs implements vs with
Quan hệKế thừa code?Phải override method?Use case
extends AChỉ method abstract"Is-a" relationship, share code
implements AKhôngTất cả"Behaves like" — commit signature only
with MKhôngCompose behavior từ mixin

9. Mixin — share code không qua inheritance

mixin Walker {
  void walk() => print('đi bộ');
}

mixin Swimmer {
  void swim() => print('bơi');
}

class Duck with Walker, Swimmer {}

void main() {
  final d = Duck();
  d.walk();  // đi bộ
  d.swim();  // bơi
}

Mixin với constraint on — chỉ áp dụng được cho class kế thừa class cụ thể:

class Animal {}

mixin Flyer on Animal {     // chỉ class extends Animal mới dùng được
  void fly() => print('bay');
}

class Bird extends Animal with Flyer {}  // OK
// class Plane with Flyer {}            // ❌ — Plane không extends Animal
🧠 Mixin ≠ multiple inheritance

Mixin compose code vào class — không phải parent. Một class có thể có nhiều mixin (vd with M1, M2, M3). Method resolution order: A → M3 → M2 → M1 → ParentOfA → Object. Đây gọi là linearization.

10. Class modifier Dart 3

Dart 3 thêm 5 keyword class modifier để kiểm soát ai được extends/implements class của bạn.

ModifierCho phép extends?Cho phép implements?Cho phép as mixin?Use case
(none)Cùng/khác libraryCùng/khác libraryKhôngDefault open
baseCùng/khác libraryChỉ cùng libraryKhôngForce extends — chống abuse implements
finalChỉ cùng libraryChỉ cùng libraryKhôngKhoá hierarchy — không cho subtype ở library khác
sealedChỉ cùng libraryChỉ cùng libraryKhôngExhaustive switch + chỉ subclass trong file
interfaceChỉ cùng libraryCùng/khác libraryKhông"Pure interface" — không cho extends ngoài lib
mixin classClass dùng được như mixin (legacy support)
// File a.dart
sealed class Result {}
class Success extends Result {}
class Failure extends Result {}

// File b.dart
import 'a.dart';
// class Other extends Result {}  // ❌ — sealed không cho extends ngoài library

// Nhưng switch trên Result là EXHAUSTIVE — compiler enforce
String describe(Result r) {
  return switch (r) {
    Success() => 'ok',
    Failure() => 'fail',
    // Không cần default — compiler biết chỉ 2 case
  };
}

11. toString, ==, hashCode

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

  @override
  String toString() => 'Point($x, $y)';

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      (other is Point && other.x == x && other.y == y);

  @override
  int get hashCode => Object.hash(x, y);
}

final a = Point(1, 2);
final b = Point(1, 2);
print(a == b);            // true
print(a.hashCode == b.hashCode); // true
print(a);                 // Point(1, 2)
⚠️ Override == phải override hashCode

Quy tắc: nếu a == b thì a.hashCode == b.hashCode. Vi phạm → hash-based collection (Set, Map) vỡ. Analyzer cảnh báo. Trong thực tế dùng package equatable hoặc freezed để auto-gen — sẽ thấy ở Flutter.

12. Pattern copyWith

Pattern chuẩn cho immutable model — Flutter dùng khắp nơi (Bloc state, theme config, etc).

class User {
  final String name;
  final int age;
  final bool premium;

  const User({required this.name, required this.age, this.premium = false});

  User copyWith({String? name, int? age, bool? premium}) {
    return User(
      name: name ?? this.name,
      age: age ?? this.age,
      premium: premium ?? this.premium,
    );
  }
}

final u1 = User(name: 'Việt', age: 25);
final u2 = u1.copyWith(age: 26);    // User(Việt, 26, false)
final u3 = u1.copyWith(premium: true); // User(Việt, 25, true)

13. Enum nâng cao (Dart 2.17+)

enum Priority {
  low(0),
  medium(1),
  high(2),
  critical(3);

  final int weight;
  const Priority(this.weight);

  bool isHigherThan(Priority other) => weight > other.weight;

  String get label => switch (this) {
    Priority.low => 'Thấp',
    Priority.medium => 'Trung bình',
    Priority.high => 'Cao',
    Priority.critical => 'Cực cao',
  };
}

print(Priority.high.label);                          // Cao
print(Priority.high.isHigherThan(Priority.low));  // true

14. Bài tập

Money class với ==, hashCode, copyWith

Implement class Money với field amount: int (cents) và currency: String. Override ==, hashCode, toString. Thêm copyWith. Test: 2 instance cùng amount + currency → == trả true.

💡 Gợi ý đáp án
class Money {
  final int amount;
  final String currency;

  const Money(this.amount, this.currency);

  Money copyWith({int? amount, String? currency}) =>
      Money(amount ?? this.amount, currency ?? this.currency);

  @override
  String toString() => '${amount / 100} $currency';

  @override
  bool operator ==(Object o) =>
      identical(this, o) ||
      (o is Money && o.amount == amount && o.currency == currency);

  @override
  int get hashCode => Object.hash(amount, currency);
}

void main() {
  final a = Money(100, 'USD');
  final b = Money(100, 'USD');
  print(a == b);          // true
  print(a.copyWith(amount: 200));  // 2.0 USD
}

Shape hierarchy

Tạo abstract class Shape với double area(). Implement 3 subclass: Circle, Rectangle, Triangle. Viết function Shape biggest(List<Shape> shapes) trả về Shape lớn nhất.

💡 Gợi ý đáp án
import 'dart:math';

abstract class Shape {
  double area();
}

class Circle extends Shape {
  final double r;
  Circle(this.r);
  @override double area() => pi * r * r;
}

class Rectangle extends Shape {
  final double w, h;
  Rectangle(this.w, this.h);
  @override double area() => w * h;
}

class Triangle extends Shape {
  final double base, height;
  Triangle(this.base, this.height);
  @override double area() => 0.5 * base * height;
}

Shape biggest(List<Shape> shapes) =>
    shapes.reduce((a, b) => a.area() > b.area() ? a : b);

void main() {
  final s = biggest([Circle(5), Rectangle(4, 8), Triangle(6, 10)]);
  print('${s.runtimeType}: ${s.area()}');
}

Logger mixin

Viết mixin Logger thêm method void log(String msg) in ra với timestamp prefix. Apply vào 2 class khác nhau (vd HttpClient, DbClient) và demo.

💡 Gợi ý đáp án
mixin Logger {
  void log(String msg) {
    print('[${DateTime.now().toIso8601String()}] $msg');
  }
}

class HttpClient with Logger {
  void get(String url) {
    log('GET $url');
  }
}

class DbClient with Logger {
  void query(String sql) {
    log('SQL: $sql');
  }
}

void main() {
  HttpClient().get('/users');     // [2026-...] GET /users
  DbClient().query('SELECT *');   // [2026-...] SQL: SELECT *
}

Singleton qua factory

Refactor pattern Singleton manual (private static + private constructor) sang dùng factory constructor. So sánh code length + clarity.

💡 Gợi ý đáp án
// Manual singleton (verbose)
class AppManualSingleton {
  static AppManualSingleton? _instance;
  static AppManualSingleton get instance => _instance ??= AppManualSingleton._();
  AppManualSingleton._();
}

// Factory singleton — gọi như constructor bình thường
class App {
  static final App _instance = App._internal();
  factory App() => _instance;
  App._internal();
}

void main() {
  print(identical(App(), App()));  // true — cùng instance
}

Factory singleton: caller gọi App() tự nhiên — không cần biết internal. Manual: caller phải gọi .instance.

Sealed Result<T> chuẩn bị cho chương 8

Tạo sealed class Result<T> với 2 subclass: Success<T>(T data)Failure(String error). Demo create + dùng cơ bản (chưa cần switch — sẽ học ở chương 8).

💡 Gợi ý đáp án
sealed class Result<T> {
  const Result();
}

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

class Failure extends Result<Never> {
  final String error;
  const Failure(this.error);
}

Result<int> divide(int a, int b) {
  if (b == 0) return Failure('chia 0');
  return Success(a ~/ b);
}

void main() {
  final r = divide(10, 2);
  if (r is Success<int>) print(r.data);     // 5
  if (r is Failure) print(r.error);
}

Chương 8 sẽ học switch (r) { case Success(:var data): ...; case Failure(:var error): ... } đẹp hơn.

15. Quiz

Q1

Constructor Point.origin() gọi từ ngoài bằng cách nào?

Xem đáp án

Đáp án: Point.origin() — gọi với cú pháp tên class.tên constructor(). Không phải Point().origin().

Q2

factory constructor có bắt buộc trả về instance mới không?

Xem đáp án

Đáp án: Không. Đây là điểm đặc biệt. Có thể trả cached instance, instance subclass, hoặc null (nếu return type nullable). Tự do hơn constructor thường.

Q3

Dart có keyword interface không?

Xem đáp án

Đáp án: Có ở Dart 3 (class modifier). Nhưng từ Dart 2, mọi class đã là implicit interface — implements AnyClass luôn work mà không cần interface keyword.

Q4

Sau class A extends B with M1, M2, method resolution order là gì?

Xem đáp án

Đáp án: A → M2 → M1 → B → Object. Đọc từ trái sang phải mixin, sau cùng đến parent. Gọi là linearization.

Q5

Override == mà không override hashCode — chuyện gì?

Xem đáp án

Đáp án: Analyzer cảnh báo (hash_and_equals lint). Hash-based collection (Set, Map) sẽ behave sai — vì hashCode mặc định là identity, không match logic ==.

Q6

sealed class Dart 3 dùng để làm gì?

Xem đáp án

Đáp án: Giới hạn subclass trong cùng library (file), enable exhaustive switch — compiler đảm bảo mọi subclass được handle. Pattern phổ biến cho state machine, Result type.

Q7

_name field — private ở scope nào?

Xem đáp án

Đáp án: Library scope (file scope nếu không export). Khác Java/C# class-scope. Đa số case 1 file = 1 library, nên ≈ file scope.

Q8

static method gọi từ instance được không?

Xem đáp án

Đáp án: Không. Chỉ qua tên class: User.all(), không user.all(). Khác JavaScript class.

16. Tổng kết

  • ✅ Class anatomy: field, method, constructor.
  • ✅ 5 loại constructor: default, named, initializer list, redirecting, factory.
  • ✅ Getter/setter — computed property, gọi như field.
  • static member — gọi qua tên class.
  • _ prefix — library-private, không class-private.
  • extends single inheritance + @override + super.
  • abstract class — không instantiate được.
  • implements — implicit interface, mọi class auto là interface.
  • mixin + with + on constraint.
  • ✅ Class modifier Dart 3: base, final, sealed, interface, mixin class.
  • ✅ Pattern: == + hashCode + copyWith.
  • ✅ Enum nâng cao với field + method (Dart 2.17+).

17. Kết nối tới các chương khác

  • Chương 5 — Null Safety: field late cho lazy init.
  • Chương 8 — Patterns: sealed class + exhaustive switch deep dive.
  • Flutter chương 2: mọi widget là class extends StatelessWidget / StatefulWidget.
  • Flutter chương 7: Bloc/Cubit dựa class hierarchy.