Chương 02 · Variables & Types

Variables & Types — xương sống của Dart

Type system là xương sống của Dart — khác hẳn JavaScript dynamic. User có nền TypeScript sẽ thấy quen, nhưng Dart strict hơn TS: sound null safety, không có any ngoại lệ. Hiểu sai chương này thì chương 4 (class) và chương 5 (null safety) sẽ lệch.

Độ dài: ~870 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1 (chạy được dart run)
🎯 Mục tiêu chương
  • Hiểu Dart type system — static + sound (sau Dart 2.12).
  • Phân biệt var, final, const, dynamic, Object, Object? — khi nào dùng cái nào.
  • Sử dụng đúng built-in types: int, double, num, String, bool, List, Set, Map.
  • Hiểu type inference vs type annotation — khi nào để Dart tự suy luận, khi nào ghi explicit.
  • Sử dụng String interpolation, raw string, multi-line string thành thạo.
  • Hiểu null không phải value bình thường — phải explicit qua nullable type T?.

1. Static type system là gì?

"Type system" mô tả cách ngôn ngữ quản lý kiểu dữ liệu của biến/biểu thức. Có 4 trục chính mà mọi ngôn ngữ phân loại theo:

Ngôn ngữStatic / DynamicStrong / WeakSound / UnsoundNull safety
JavaScriptDynamicWeak (auto coerce)N/AKhông
PythonDynamicStrongN/AKhông
TypeScriptStatic (gradual)StrongUnsound (do any)Strict mode optional
JavaStaticStrongSound (mostly)Không (có null bất kỳ đâu)
Dart 3StaticStrongSoundSound, mặc định
KotlinStaticStrongSoundSound, mặc định
RustStaticStrongSoundKhông có null — dùng Option

Định nghĩa nhanh:

  • Static — type biết tại compile-time. Analyzer kiểm tra trước khi chạy. Ngược lại: dynamic (type biết runtime).
  • Strong — không auto-convert. 1 + "1" ở Dart lỗi compile. Ở JavaScript trả "11" (auto coerce string).
  • Sound — type analyzer chứng minh được không lỗi runtime. TS có any nên không sound. Dart không có any (chỉ có dynamic nhưng phải opt-in explicit) — sound.
  • Sound null safety — analyzer chứng minh được biến nullable hoặc non-nullable. Compile fail nếu vi phạm.
🧠 Vì sao "sound" quan trọng?

Khi type system sound, bạn có một guarantee mạnh: nếu code compile được, một class lỗi runtime (ví dụ NullPointerException) không thể xảy ra. Mọi lỗi đã bị bắt ở compile-time.

TypeScript "gradual typing" cho phép any escape — type check tại edge này, không tại edge kia. Code TS compile vẫn có thể throw Cannot read property of undefined. Dart 3 không có lỗ hổng đó.

2. Khai báo biến — 4 cách

Dart cho 4 cách khai báo biến. Hiểu rõ khác biệt = code clean ngay từ đầu.

// 1. var — type inferred từ giá trị khởi tạo
var name = 'Việt';        // type: String
var age = 25;             // type: int

// 2. final — gán 1 lần, immutable variable
final birthday = DateTime.now();  // OK — runtime value

// 3. const — compile-time constant
const pi = 3.14159;          // OK — biết tại compile time
// const now = DateTime.now();  // Lỗi: runtime value

// 4. Type annotation explicit
int count = 0;
String message = 'hello';
List<int> numbers = [1, 2, 3];

Bảng quyết định:

CáchReassign?Compile-time?Type cố định?Khi nào dùng
var x = 5KhôngCó (inferred)Default cho biến local
final x = 5KhôngKhôngCó (inferred)Khi không cần reassign (đa số case)
const x = 5KhôngCó (inferred)Hằng số biết trước compile-time
int x = 5KhôngCó (explicit)Khi inference fail hoặc muốn ép rõ
💡 Idiomatic Dart: prefer final

Lint prefer_final_locals khuyến khích dùng final mặc định cho biến local. Lý do: immutable variable an toàn hơn, dễ đọc — khi reassign mới chuyển var. Áp dụng cùng nguyên tắc với JS const > let.

final vs const — khác biệt quan trọng

Cả 2 đều immutable nhưng có 1 điểm khác chí mạng:

final now1 = DateTime.now();      // OK — runtime value
// const now2 = DateTime.now();   // ❌ Lỗi compile

final list1 = [1, 2, 3];
const list2 = [1, 2, 3];

list1.add(4);  // OK — final chỉ khoá biến, không khoá nội dung
// list2.add(4);  // ❌ Runtime error — const list là unmodifiable

final: biến gán 1 lần, giá trị có thể là runtime. Nội dung object (vd List) vẫn mutate được.
const: biến và toàn bộ giá trị phải biết tại compile-time. Nội dung immutable hoàn toàn.

const tạo canonical instance

Đây là feature đặc biệt của const:

const a = [1, 2, 3];
const b = [1, 2, 3];

print(identical(a, b));  // true — cùng instance trong memory!

var c = [1, 2, 3];
var d = [1, 2, 3];

print(identical(c, d));  // false — 2 instance khác nhau

Dart "canonicalize" const value — 2 const giống hệt nội dung dùng cùng một instance trong memory. Tiết kiệm bộ nhớ + cho phép compare bằng identical. Đặc biệt quan trọng cho Flutter widget — sẽ thấy ở sub-pillar Flutter.

3. Type inference — khi nào fail?

Dart analyzer infer type từ giá trị khởi tạo. Đa số trường hợp work tốt — nhưng có case fail:

var a = 5;              // infer int — OK
var b = 3.14;           // infer double — OK
var c = [1, 2, 3];     // infer List<int> — OK
var d = [1, 'a'];        // infer List<Object> — vẫn OK nhưng có thể không phải ý bạn
var e;                  // ❌ infer dynamic — analyzer warn

Khi infer ra dynamic (mất type safety), nên annotate rõ:

int? value;             // nullable int, default null
value = 5;
// value = 'a';        // ❌ analyzer bắt
⚠️ vardynamic

Đừng nhầm:

var x = 5;     // infer int — TYPE CỐ ĐỊNH
x = 'a';          // ❌ Lỗi: String không gán được vào int

dynamic y = 5; // type dynamic — TẮT TYPE CHECK
y = 'a';          // ✅ OK — vì dynamic chấp nhận tất cả

var chỉ là shorthand cho "type rõ ràng, đừng bắt tôi viết". Không phải "dynamic".

4. Numeric types — int, double, num

Dart có 2 type số: int (số nguyên) và double (số thực — IEEE 754 64-bit). Cả 2 cùng kế thừa num.

int count = 42;
double price = 19.99;
num any = 10;        // num chấp nhận int hoặc double
any = 10.5;            // OK

// int và double KHÔNG auto convert
int a = 3.0;          // ❌ Lỗi compile
double b = 3;          // ❌ Lỗi compile (Dart 2.1+ cho phép, Dart 3 strict)

// Convert thủ công
double c = 3.toDouble();   // 3.0
int d = 3.7.toInt();       // 3 (truncate)
int e = 3.7.round();       // 4 (round)

Parse string thành số

int n = int.parse('42');         // throws FormatException nếu fail
int? m = int.tryParse('42a');   // trả null thay throw, safer

double p = double.parse('3.14');
double? q = double.tryParse('abc');  // null

BigInt — số cực lớn

BigInt huge = BigInt.parse('12345678901234567890');
print(huge * huge);  // không overflow như int 64-bit

int Dart là 64-bit signed (-9.2 × 10¹⁸ đến +9.2 × 10¹⁸). Vượt giới hạn → wrap-around hoặc dùng BigInt.

5. String — interpolation + multiline + raw

String literal

String a = 'single quote';   // idiomatic Dart
String b = "double quote";   // cũng OK
String c = "Cô ấy nói: 'Xin chào'";  // dùng double quote khi content có '
String d = 'It\'s OK';       // escape với \

String interpolation

final name = 'Việt';
final age = 25;
final hobbies = ['code', 'đọc'];

// $identifier — chỉ với identifier đơn
print('Tôi tên $name, $age tuổi');

// ${expression} — bất kỳ expression
print('Sang năm tôi ${age + 1} tuổi');
print('Tổng hobbies: ${hobbies.length}');

// Toán cũ Java-style — không cần
// 'Tôi tên ' + name + ', ' + age.toString() + ' tuổi'  ← đừng viết thế
🔀 So với JS template literal
// JavaScript
const msg = `Tôi tên ${name}, ${age} tuổi`;

// Dart — gần giống, khác ở dấu mở
final msg = 'Tôi tên $name, $age tuổi';

JS dùng backtick + ${...}. Dart dùng quote thường + $name shorthand cho identifier.

Multi-line string

String html = '''
<div>
  <h1>$title</h1>
</div>
''';

// Hoặc double quote triple
String json = """
{
  "name": "$name",
  "age": $age
}
""";

Raw string — không xử lý escape

// Normal — \n thành newline thực sự
String path1 = 'C:\\Users\\Việt';  // phải escape \

// Raw — \ giữ nguyên
String path2 = r'C:\Users\Việt';   // chữ r trước string

// Tiện cho regex
final emailRegex = RegExp(r'^[\w-]+@[\w-]+\.[\w]{2,}$');

String là immutable

final s = 'hello';
// s[0] = 'H';   // ❌ Không tồn tại operator []= cho String
final upper = s.toUpperCase();  // Trả string mới 'HELLO'

// Common method:
print(s.length);          // 5
print(s.contains('ll'));  // true
print(s.replaceAll('l', 'L'));  // 'heLLo'
print(s.split(''));         // ['h','e','l','l','o']

bool — strict, không có truthy

bool isOk = true;
bool done = false;

// Khác JS: không có truthy/falsy
if (1) { }              // ❌ Lỗi compile — 1 không phải bool
if ('hello') { }        // ❌ Lỗi compile
if (someList) { }        // ❌ Lỗi compile

// Phải check explicit
if (someList.isNotEmpty) { }   // ✅
if (count > 0) { }            // ✅
if (name != null) { }         // ✅
🔥 Gotcha cho dev JS

Nếu bạn viết JS lâu, thói quen if (someValue) để check truthy sẽ bị reject ngay ở Dart. Lúc đầu khó chịu, sau quen → code rõ ràng hơn (không có magic coerce).

6. null và nullable type

Đây là điểm khác biệt lớn nhất Dart so với JS/Java/Python.

int a = null;          // ❌ Lỗi compile — int là non-nullable
int? b = null;         // ✅ — int? nullable
String? name;            // ✅ — mặc định null vì nullable
String message;          // ❓ — phải gán trước khi đọc

print(message);          // ❌ Lỗi compile — non-nullable chưa gán

Quy tắc vàng:

  • T (không có ?) — type non-nullable. Không bao giờ là null.
  • T? (có ?) — type nullable. Có thể là T hoặc null.
  • Mọi biến non-nullable phải được gán (initializer hoặc trong constructor) trước khi đọc lần đầu.

Demo:

void greet(String? name) {
  // Không gọi trực tiếp name.toUpperCase() — name có thể null
  // name.toUpperCase();  // ❌ Lỗi compile

  // 3 cách handle:

  // (a) Check null trước → flow promotion
  if (name != null) {
    print(name.toUpperCase());  // OK — analyzer biết name không null trong block
  }

  // (b) Null-aware operator
  print(name?.toUpperCase());     // Trả null nếu name null

  // (c) Provide default
  print((name ?? 'guest').toUpperCase());
}
📘 Chương 5 đi sâu

Đây mới là basic. Chương 5 (Null Safety & Error Handling) sẽ deep dive 5 null-aware operator, ! non-null assertion, late keyword, definite assignment analysis, custom exception.

7. dynamic vs Object?

Khi cần "type bất kỳ", có 2 lựa chọn:

// dynamic — tắt type check (giống TS any)
dynamic a = 5;
a = 'hello';
a = [1, 2];
a.whateverMethod();   // Compile OK, RUNTIME crash nếu sai

// Object? — type cha của mọi type, type check vẫn hoạt động
Object? b = 5;
b = 'hello';
b = [1, 2];
// b.toUpperCase();    // ❌ Lỗi compile — Object? không có toUpperCase

// Phải cast hoặc check type:
if (b is String) {
  print(b.toUpperCase());   // OK — b promoted thành String trong block
}
TypeChấp nhận giá trị?Type check?Use case
dynamicBất kỳTắt — runtime crash nếu saiJSON parse, FFI, interop JS — escape hatch
Object?Bất kỳ kể cả nullBật — phải cast hoặc is checkDefault cho "tôi không biết type"
ObjectBất kỳ trừ nullBậtKhi chắc chắn non-null nhưng type bất kỳ
⚠️ Tránh dynamic

dynamic mất hết safety của Dart. Chỉ dùng khi không có cách khác (vd parse JSON Map<String, dynamic>). Mặc định dùng Object? + is check.

8. Type promotion / Flow analysis

Dart analyzer "thông minh" — sau khi kiểm tra type, nó thu hẹp type trong block tiếp theo:

void describe(Object? value) {
  if (value is String) {
    // Trong block này, value được promoted thành String
    print('String length: ${value.length}');
  } else if (value is int) {
    print('Int double: ${value * 2}');
  } else if (value != null) {
    // value promoted thành Object (loại null)
    print('Other: ${value.toString()}');
  }
}

Flow analysis cũng hoạt động với null check:

String? name = getName();

if (name != null) {
  print(name.length);  // OK — name promoted thành String
}

// Pattern early return cũng work:
if (name == null) return;
print(name.length);    // OK — sau early return, name chắc chắn không null
🔥 Hạn chế: chỉ work với local variable

Flow analysis chỉ work với local variable. Field của class bị invalidate khi gọi method bất kỳ (vì method có thể đổi field).

class User {
  String? name;

  void show() {
    if (name != null) {
      // name.length;  // ❌ — analyzer không promote field
      // Pattern: copy ra local
      final n = name;
      if (n != null) print(n.length);  // OK
    }
  }
}

9. Collection types — sơ lược

3 collection cơ bản: List, Set, Map. Chỉ điểm qua — chương 6 sẽ deep dive.

// List — sequence ordered
List<int> numbers = [1, 2, 3];
var names = ['Việt', 'Anh', 'Phương'];  // inferred List<String>

// Set — unique unordered
Set<String> tags = {'a', 'b', 'a'};  // {a, b}

// Map — key-value
Map<String, int> ages = {
  'Việt': 25,
  'Anh': 30,
};

print(numbers[0]);   // 1
print(ages['Việt']);  // 25

10. Type hierarchy Dart

Mọi type Dart thừa kế từ Object?. Sơ đồ chính:

Object? Object Null num String bool List<T> Map<K,V> Set<T> int double Mọi non-nullable type thừa kế Object (không null). Object? = Object ∪ Null = "có thể null". Để gán null: type phải là T? (vd int? — int ∪ Null).
Type hierarchy đơn giản hoá của Dart

11. Bài tập

5 cách khai báo biến

Khai báo 5 biến trong một file vars.dart: 1 dùng var, 1 final, 1 const, 1 explicit type, 1 nullable. Mỗi cái 1 dòng kèm comment giải thích. Chạy dart analyze — không có warning.

💡 Gợi ý đáp án
void main() {
  var name = 'Việt';                       // var — inferred String
  final age = DateTime.now().year - 2000;  // final — runtime value OK
  const pi = 3.14159;                      // const — compile-time
  double price = 19.99;                    // explicit type
  String? optionalLabel;                   // nullable, default null

  print('$name, $age tuổi, pi=$pi, price=$price, label=$optionalLabel');
}

formatPrice với interpolation

Viết function String formatPrice(int amount, String currency) trả về string format "1,500,000 VND" (có dấu phẩy ngăn cách hàng nghìn). Dùng interpolation + helper.

💡 Gợi ý đáp án
String formatPrice(int amount, String currency) {
  final str = amount.toString();
  final buf = StringBuffer();
  for (var i = 0; i < str.length; i++) {
    if (i > 0 && (str.length - i) % 3 == 0) buf.write(',');
    buf.write(str[i]);
  }
  return '$buf $currency';
}

void main() {
  print(formatPrice(1500000, 'VND'));  // "1,500,000 VND"
  print(formatPrice(99, 'USD'));        // "99 USD"
}

Chuyên nghiệp hơn: dùng package intl với NumberFormat. Nhưng tự implement giúp hiểu loop + interpolation.

Compile error spotting

Cho biết dòng nào sau lỗi compile, vì sao:

(a) int x = 3.0;
(b) num y = 3;
(c) double z = 3;
(d) var w; w = 5;
(e) const t = DateTime.now();
(f) final list = [1, 2]; list.add(3);
(g) int? n = null; print(n.toString());
💡 Gợi ý đáp án
  • (a) ❌ Lỗi — 3.0 là double, không gán được vào int (Dart strict).
  • (b) ✅ OK — num chấp nhận cả int và double.
  • (c) ❌ Lỗi — 3 là int literal, không gán được vào double (cần 3.0 hoặc 3.toDouble()).
  • (d) ❌ Warning — var w; không có initializer → infer dynamic. Analyzer warn.
  • (e) ❌ Lỗi — DateTime.now() là runtime value, không thể const.
  • (f) ✅ OK — final chỉ khoá biến (list reference), không khoá nội dung list.
  • (g) ❌ Lỗi — n.toString() không safe trên int?. Phải n?.toString() hoặc check null.

Const canonical instance

Viết file demo: tạo 2 biến const list1 = [1, 2, 3]const list2 = [1, 2, 3]. Gọi identical(list1, list2) — kết quả? Lặp lại với var thay const — kết quả? Giải thích vì sao.

💡 Gợi ý đáp án
void main() {
  const a = [1, 2, 3];
  const b = [1, 2, 3];
  print(identical(a, b));  // true

  var c = [1, 2, 3];
  var d = [1, 2, 3];
  print(identical(c, d));  // false
}

const Dart "canonicalize" — 2 const cùng nội dung dùng cùng instance memory. Tiết kiệm bộ nhớ + cho phép identity check.

var tạo instance riêng mỗi lần — 2 list khác địa chỉ memory dù cùng nội dung.

Đây là lý do Flutter khuyến khích const cho widget — tránh rebuild thừa khi 2 widget cùng config.

Refactor dynamic → explicit type

Cho đoạn code:

dynamic parseUser(dynamic json) {
  return {
    'name': json['name'],
    'age': json['age'],
  };
}

void main() {
  dynamic user = parseUser({'name': 'Việt', 'age': 25});
  print(user['name']);
}

Refactor để loại bỏ dynamic. Hint: dùng Map<String, dynamic> cho JSON (compromise), và type rõ ràng cho kết quả.

💡 Gợi ý đáp án
Map<String, Object?> parseUser(Map<String, dynamic> json) {
  return {
    'name': json['name'] as String?,
    'age': json['age'] as int?,
  };
}

void main() {
  final user = parseUser({'name': 'Việt', 'age': 25});
  print(user['name']);  // type Object? — phải cast nếu dùng
}

Map<String, dynamic> ở input vẫn cần — đây là quy ước cho JSON. Nhưng output có type cụ thể hơn. Chương 8 sẽ học cách dùng records hoặc class để type-safe hoàn toàn.

12. Quiz

Q1

var x = 5; rồi x = '5'; — chuyện gì xảy ra?

Xem đáp án

Đáp án: Lỗi compile. x đã được inferred type int tại dòng khai báo. '5' là String — không gán được. vardynamic.

Q2

finalconst — cái nào tạo canonical instance?

Xem đáp án

Đáp án: const. Hai const cùng nội dung dùng cùng instance memory (canonicalize). final chỉ khoá biến gán 1 lần, nội dung vẫn là instance riêng.

Q3

1 == 1.0 ở Dart trả về gì?

Xem đáp án

Đáp án: true. intdouble cùng kế thừa num, so sánh giá trị qua num equality. Khác Java/C# strict (so sánh type).

Q4

if (1) { ... } ở Dart hợp lệ không?

Xem đáp án

Đáp án: Không. Lỗi compile. Dart không có truthy/falsy — if chỉ chấp nhận bool thuần. Khác JS hoàn toàn.

Q5

Type nào là parent chung của intdouble?

Xem đáp án

Đáp án: num. intdouble đều extends num. num extends Object.

Q6

int? x; chưa gán giá trị — đọc x trả gì?

Xem đáp án

Đáp án: null. Nullable type mặc định là null. Khác với non-nullable int x; — đọc x trước khi gán → lỗi compile (definite assignment).

Q7

int x; (non-nullable) chưa gán giá trị, sau đó đọc x — chuyện gì?

Xem đáp án

Đáp án: Lỗi compile — "Non-nullable variable 'x' must be assigned before it can be used." Đây gọi là definite assignment analysis. Phải gán trước khi đọc.

Q8

Khác biệt 'a$b''a${b}' là gì?

Xem đáp án

Đáp án: Không khác — cả 2 đều interpolation cho biến b. ${...} với braces cần thiết khi expression không phải identifier đơn (vd ${user.name} hoặc ${a + b}). $identifier shorthand cho biến đơn.

13. Tổng kết

  • ✅ Hiểu Dart static + sound type system (vs JS dynamic, TS gradual).
  • ✅ 4 cách khai báo biến: var (inferred), final (runtime immutable), const (compile-time + canonical), explicit type.
  • ✅ Built-in types: int, double, num, String, bool, List, Set, Map.
  • ✅ String interpolation: $name${expr}. Multi-line '''. Raw r'...'.
  • bool strict — không có truthy/falsy.
  • ✅ Nullable T? vs non-nullable T — sound null safety.
  • dynamicvar, Object? vs dynamic.
  • ✅ Flow analysis — analyzer thu hẹp type sau is/null check.
🧠 Mental model rút ra

Dart không "hack" được type system. Mọi escape (dynamic, !, as cast) phải explicit. Kết quả: code compile được hầu như không bao giờ throw TypeError hay NullPointer runtime. Đổi lại: bạn phải viết type nhiều hơn JS, ít hơn Java/TS — Dart cân giữa concise và explicit.

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

  • Chương 3 — Functions & Control Flow: function params có type, required named param.
  • Chương 4 — Classes: class field có type, final field, const constructor.
  • Chương 5 — Null Safety: deep dive 5 null-aware operator, late, !.
  • Chương 6 — Collections: List/Set/Map đầy đủ + higher-order method.
  • Flutter chương 2 — Widget Tree: widget property hầu hết non-nullable, const constructor optimize.