- Hiểu sound null safety là gì, vì sao Dart 2.12+ làm điều này.
- Phân biệt
T(non-nullable) vàT?(nullable). - Dùng đủ 5 null-aware operator:
?.,??,??=,...?,?[]. - Dùng
!non-null assertion đúng chỗ + hiểu rủi ro. - Hiểu
latekeyword: late init + lazy late. - Phân biệt
ExceptionvsError. - Bắt exception đúng:
try/catch/finally,on Type catch,rethrow. - Tự định nghĩa exception class.
- Xử lý lỗi async với
try/catch+await.
1. Vì sao có null safety?
Năm 2009, Tony Hoare — người tạo ra null reference năm 1965 — gọi đây là "billion dollar mistake".
Lý do: null reference gây ra uncountable bug runtime ở mọi ngôn ngữ có nó (Java, C#, JS, Python).
Dart trước version 2.12 (2021): có null tự do — biến bất kỳ có thể là null. Mỗi access có thể throw.
Dart 2.12+: sound null safety. Compile-time guarantee: nếu type là T (không ?),
giá trị không bao giờ là null. Mọi nullable phải explicit T?.
"Sound" = analyzer chứng minh được — không phải "lint cảnh báo". Code compile được nghĩa là null safety đảm bảo. TS strict mode chỉ "cảnh báo" — Dart 3 cấm.
Hệ quả: ở Dart code mà NullPointerException kiểu Java không tồn tại. Bạn không bao giờ phải debug "tại sao biến này lại null".
2. Nullable T? vs non-nullable T
// Non-nullable — không bao giờ null
int count = 0;
count = null; // ❌ Lỗi compile
// Nullable — có thể null
int? maybeCount = 0;
maybeCount = null; // OK
// Default value cho nullable: null
String? name; // = null mặc định
print(name); // null
// Non-nullable chưa gán → lỗi compile khi đọc
String message;
print(message); // ❌ "Non-nullable variable 'message' must be assigned"
3. Definite assignment analysis
Analyzer kiểm tra biến non-nullable phải được gán chắc chắn trước khi đọc:
void demo(int input) {
String name;
if (input > 0) {
name = 'positive';
} else if (input < 0) {
name = 'negative';
} // ⚠ Thiếu case input == 0
print(name); // ❌ Lỗi — không chắc chắn gán
}
Fix: thêm else hoặc gán default:
void demo(int input) {
String name;
if (input > 0) {
name = 'positive';
} else if (input < 0) {
name = 'negative';
} else {
name = 'zero';
}
print(name); // ✅
}
4. 5 null-aware operator
?. — Safe call
String? name;
print(name?.length); // null (không throw)
name = 'Việt';
print(name?.length); // 4
// Chain
print(user?.address?.city?.toLowerCase());
?? — Default value
String? name;
final displayName = name ?? 'Khách'; // 'Khách'
name = 'Việt';
final d2 = name ?? 'Khách'; // 'Việt'
??= — Assign nếu null
String? name;
name ??= 'default'; // gán vì null
print(name); // 'default'
name ??= 'override'; // không gán vì đã có giá trị
print(name); // 'default'
...? — Spread null-safe
List<int>? maybeXs;
final combined = [0, ...?maybeXs, 99];
print(combined); // [0, 99] — maybeXs null bị skip
maybeXs = [1, 2, 3];
final c2 = [0, ...?maybeXs, 99];
print(c2); // [0, 1, 2, 3, 99]
?[] — Index null-safe
Map<String, int>? map;
print(map?['key']); // null (map null)
map = {'a': 1};
print(map?['a']); // 1
print(map?['b']); // null (key không tồn tại)
Bảng tóm tắt 5 null-aware operator:
| Operator | Khi trái null | Khi trái không null | Use case |
|---|---|---|---|
a?.method() | Trả null | Gọi method bình thường | Chain method an toàn |
a ?? b | Trả b | Trả a | Default value |
a ??= b | Gán a = b | Không gán | Lazy init |
...?a | Skip (rỗng) | Spread a | Compose collection optional |
a?[key] | Trả null | Index bình thường | Map/List optional |
! — Non-null assertion
! promote T? thành T. Nếu thực sự là null → runtime exception.
String? maybeName = getName();
// Dùng ! khi BẠN biết chắc không null nhưng analyzer không
print(maybeName!.length); // promote → String, throw nếu null
// Cẩn thận — đây là escape hatch
final users = <User>[];
final first = users.firstOrNull!; // throw vì list rỗng
! nếu được
! bypass null safety guarantee — runtime risk. Pattern tốt hơn:
- Check null trước → flow promotion:
if (x != null) x.method(); - Default value:
(x ?? defaultValue).method() - Early return:
if (x == null) return;
Chỉ dùng ! khi analyzer không đủ smart để chứng minh non-null
(ví dụ value từ Map sau check containsKey).
5. Flow analysis / Type promotion
Analyzer thu hẹp type sau check null:
void demo(String? name) {
if (name != null) {
// Trong block này, name PROMOTED thành String
print(name.length); // ✅ — không cần !
}
// Pattern early return cũng work
if (name == null) return;
print(name.length); // ✅ — sau early return, chắc chắn không null
}
Field của class không được promote (vì method khác có thể đổi field giữa chừng):
class User {
String? name;
void show() {
if (name != null) {
print(name.length); // ❌ — field không promoted
}
// Pattern: copy field ra local
final n = name;
if (n != null) {
print(n.length); // ✅ — local n được promote
}
}
}
6. late keyword
Late initialization
class User {
late String name; // "tôi hứa sẽ gán trước khi đọc"
void init(String v) {
name = v;
}
}
final u = User();
// print(u.name); // ❌ Runtime: LateInitializationError
u.init('Việt');
print(u.name); // ✅ Việt
late final — lazy init
class Config {
late final String token = _loadToken(); // chỉ gọi khi đọc lần đầu
String _loadToken() {
print('Loading...');
return 'abc123';
}
}
final c = Config();
print('created'); // created
print(c.token); // Loading... abc123
print(c.token); // abc123 (không loading lại)
late- Dependency injection:
late final Database db;set ở runtime. - Cyclic init: 2 object reference nhau — không thể init đồng thời.
- Lazy expensive computation: chỉ tính khi cần.
- StatefulWidget Flutter:
late final controller = TextEditingController();.
7. Exception vs Error
// Exception — lỗi có thể recover, nên catch
throw FormatException('Invalid JSON');
throw HttpException('404 Not Found');
// Error — bug programming, KHÔNG nên catch
throw RangeError('Index out of range');
throw StateError('Object in invalid state');
throw ArgumentError('value cannot be negative');
Bảng:
Exception | Error | |
|---|---|---|
| Semantic | Lỗi có thể xảy ra, dev có thể handle | Bug programming, dev cần FIX code |
| Ví dụ | FormatException, HttpException, FileSystemException | RangeError, ArgumentError, StateError, AssertionError |
| Catch? | ✅ Có — pattern thường gặp | ❌ Không nên — sửa code thay vì catch |
| Implements | implements Exception | extends Error |
8. try / catch / finally
try {
final result = int.parse('abc');
} catch (e) {
print('Lỗi: $e'); // FormatException
} finally {
print('Cleanup'); // Luôn chạy
}
on Type catch — bắt type cụ thể
try {
doStuff();
} on FormatException catch (e) {
print('Parse fail: ${e.message}');
} on HttpException catch (e) {
print('Network fail: $e');
} on Exception catch (e) {
print('Generic exception: $e');
} catch (e, stackTrace) {
print('Unknown: $e');
print(stackTrace);
}
rethrow — giữ stack trace
int parsePositive(String s) {
try {
final n = int.parse(s);
if (n < 0) throw ArgumentError('Negative not allowed');
return n;
} on FormatException {
print('Log: parse failed for "$s"');
rethrow; // throw lại, giữ stack trace gốc
}
}
rethrow khác throw e: rethrow giữ stack trace gốc — dev debug được nơi exception sinh ra ban đầu.
throw e tạo stack trace mới ở dòng throw.
9. Custom exception class
class ApiException implements Exception {
final int statusCode;
final String message;
const ApiException(this.statusCode, this.message);
@override
String toString() => 'ApiException($statusCode): $message';
}
Future<User> fetchUser(int id) async {
final response = await http.get(...);
if (response.statusCode != 200) {
throw ApiException(response.statusCode, response.body);
}
return User.fromJson(response.body);
}
// Usage
try {
final u = await fetchUser(1);
} on ApiException catch (e) {
print('API lỗi: ${e.statusCode}');
}
implements Exception, không extends
Exception Dart là interface (abstract class), không phải class concrete. Idiomatic implements Exception.
Error là class concrete — extends Error khi tạo custom error.
10. Async error handling
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 1));
throw Exception('Network down');
}
// Cách 1: try/catch quanh await — idiomatic
Future<void> main() async {
try {
final data = await fetchData();
print(data);
} catch (e) {
print('Lỗi: $e');
}
}
// Cách 2: .catchError — legacy nhưng vẫn dùng được
void main2() {
fetchData()
.then((data) => print(data))
.catchError((e) => print('Lỗi: $e'));
}
Flutter pitfall: callback (vd onPressed) không await, nếu throw → unhandled async exception:
ElevatedButton(
onPressed: () async {
await fetchData(); // nếu throw, không có gì catch
},
...
)
// Pattern an toàn: wrap try/catch trong callback
ElevatedButton(
onPressed: () async {
try {
await fetchData();
} catch (e) {
_showError(e.toString());
}
},
...
)
11. Bài tập
displayCity với null-aware
Cho model:
class User { final String name; final Address? address; ... }
class Address { final String? city; ... }
Viết String displayCity(User u) trả về city hoặc 'Không có địa chỉ'.
Dùng null-aware operator, không dùng if/else.
💡 Gợi ý đáp án
String displayCity(User u) =>
u.address?.city ?? 'Không có địa chỉ';
Single line. ?. safe chain, ?? default.
Refactor ! → if-check
Refactor đoạn code dùng ! quá nhiều thành code an toàn:
void show(User? u) {
print(u!.name!.toUpperCase());
print(u!.address!.city!);
}
💡 Gợi ý đáp án
void show(User? u) {
if (u == null) return;
print(u.name?.toUpperCase() ?? '(không tên)');
print(u.address?.city ?? '(không địa chỉ)');
}
Early return + null-aware. Không còn ! nào.
late final Database injection
Implement pattern late final Database db với factory để inject database.
Test: gọi db.query() trước khi setDb → expect runtime LateInitializationError.
💡 Gợi ý đáp án
class App {
late final Database db;
void setDb(Database d) {
db = d;
}
}
void main() {
final app = App();
try {
app.db.query(); // throws LateInitializationError
} catch (e) {
print('Error: $e');
}
app.setDb(Database());
app.db.query(); // OK
}
Custom ApiException
Định nghĩa class ApiException với 3 field: statusCode, message, endpoint.
Override toString. Throw từ function fetchUser mock, catch ở caller, log.
💡 Gợi ý đáp án
class ApiException implements Exception {
final int statusCode;
final String message;
final String endpoint;
const ApiException(this.statusCode, this.message, this.endpoint);
@override
String toString() => 'ApiException($statusCode @ $endpoint): $message';
}
Future<String> fetchUser(int id) async {
if (id < 0) throw ApiException(400, 'Invalid id', '/users/$id');
return 'user_$id';
}
void main() async {
try {
await fetchUser(-1);
} on ApiException catch (e) {
print(e);
}
}
Async fetch with 3-tier error
Viết async function fetchAndParse(String url) handle 3 loại lỗi: network (NetworkException),
parse (FormatException), khác (generic catch). Mỗi loại trả fallback khác.
💡 Gợi ý đáp án
class NetworkException implements Exception {}
Future<Map> fetchAndParse(String url) async {
try {
final response = await mockFetch(url);
return jsonDecode(response);
} on NetworkException {
return {'error': 'offline'};
} on FormatException {
return {'error': 'invalid response'};
} catch (e) {
return {'error': 'unknown: $e'};
}
}
12. Quiz
int x = null; ở Dart 3 — chuyện gì?
Xem đáp án
Đáp án: Lỗi compile. int non-nullable. Phải dùng int? hoặc gán giá trị khác null.
late String name; chưa gán, đọc — chuyện gì?
Xem đáp án
Đáp án: Lỗi runtime LateInitializationError: Field 'name' has not been initialized. Compile OK vì late = hứa sẽ gán; runtime kiểm tra.
obj!.method() — ! làm gì?
Xem đáp án
Đáp án: Promote T? thành T. Tại runtime: nếu thực sự null → throw TypeError: Null check operator used on a null value.
a ?? b — khi nào trả b?
Xem đáp án
Đáp án: Khi a là null. Khác với JS || (also trả khi a là 0/''/false). Dart ?? chỉ kiểm null.
a ??= b — khi nào assign?
Xem đáp án
Đáp án: Khi a đang null. Nếu a đã có giá trị → không gán.
Sau if (x != null) { ... x.method(); ... } với x là local int? — trong block x cần ! không?
Xem đáp án
Đáp án: Không. Flow promotion: local variable sau check null được tự promote thành non-null. Nhưng nếu x là field, cần copy ra local trước.
Catch Error (vd RangeError) — nên hay không?
Xem đáp án
Đáp án: Không nên. Error là bug programming — nên FIX code, không catch. Catch chỉ với Exception family.
rethrow khác throw e ở điểm gì?
Xem đáp án
Đáp án: rethrow giữ stack trace gốc — debug đúng nơi exception sinh ra. throw e tạo stack trace mới ở dòng throw, mất context gốc.
13. Tổng kết
- ✅ Sound null safety = compile-time guarantee.
- ✅
T?nullable,Tnon-nullable. Mọi escape phải explicit. - ✅ 5 null-aware operator:
?.,??,??=,...?,?[]. - ✅
!non-null assertion — bypass nhưng runtime risk. - ✅ Flow promotion — chỉ work với local var (không field).
- ✅
late= "hứa sẽ gán";late final = expr= lazy init. - ✅
ExceptionvsError— semantic khác. - ✅
try/catch/on/finally/rethrow. - ✅ Async error:
try/catchquanhawaitidiomatic.
14. Kết nối
- Chương 4 — class field
late. - Chương 7 — Async/Stream — Future error đầy đủ.
- Flutter chương 6+ — state nullable / non-nullable,
mountedcheck pattern.