Skip to Content
Dart📘 Ngôn ngữ DartAnnotations (@) và Code Generation

Annotations (@) và Code Generation trong Dart

Trong ngôn ngữ Dart, Annotations (hay còn gọi là metadata) là một cách để đánh dấu hoặc cung cấp thông tin thêm cho code của bạn (ví dụ như class, phương thức, thư viện, hoặc biến). Annotations luôn bắt đầu bằng ký tự @.

Nếu bạn từng code Java, Kotlin (với Retrofit @Get(), @Post()) hay TypeScript, bạn sẽ thấy khái niệm này rất quen thuộc.

Dart cung cấp sẵn một số annotations tích hợp như @override, @deprecated, nhưng sức mạnh thực sự của nó nằm ở việc tạo ra các Custom Annotations kết hợp với Code Generation (sinh mã tự động), rất phổ biến trong các thư viện Flutter như json_serializable, freezed, hay retrofit.


1. Annotations cơ bản tích hợp sẵn

Hai annotations phổ biến nhất được tích hợp sẵn trong Dart:

  • @override: Cho trình biên dịch biết rằng phương thức này đang ghi đè một phương thức từ lớp cha. Nó giúp tránh lỗi typo (đánh máy sai) tên phương thức.
  • @deprecated (hoặc @Deprecated('lý do')): Đánh dấu một đoạn code đã cũ và không nên dùng nữa, trình biên dịch sẽ cảnh báo (warning) nếu ai đó cố tình gọi nó.
class Animal { void makeSound() { print("Animal sound"); } @deprecated void oldMethod() { print("Don't use this anymore"); } } class Dog extends Animal { @override void makeSound() { print("Woof"); } }

2. Tạo Custom Annotation của riêng bạn

Trong Dart, mọi Annotation đơn giản chỉ là một object (đối tượng) mang trạng thái const (hằng số lúc biên dịch). Bạn có thể tạo nó bằng cách khai báo một lớp với hằng số constructor (const constructor).

// Định nghĩa một annotation class Todo { final String who; final String what; // Bắt buộc phải là const constructor const Todo(this.who, this.what); } // Định nghĩa một annotation không chứa tham số (như @JsonSerializable) class Route { const Route(); } const route = Route(); // Có thể khởi tạo sẵn một hằng số // Sử dụng @Todo('Bumbii', 'Cần tối ưu hàm này vào ngày mai') void doSomething() { print('Làm gì đó...'); } @route // Dùng hằng số đã khởi tạo class HomePage {}

Bạn đã đặt thành công annotation lên code của mình. Nhưng tại sao chạy thử lại không có gì xảy ra? Đó là vì annotations tự nó không làm gì cả, nó chỉ đóng vai trò như một thẻ định danh (label/metadata). Bạn cần một công cụ nào đó để phân tích thông tin này và thực hiện hành động tương ứng.


3. Tại sao Dart/Flutter lại dùng Code Generation? (Vấn đề của Reflection)

Ở các ngôn ngữ như Java/C#, người ta dùng Reflection để đọc annotation trong lúc ứng dụng đang chạy (runtime). Dart cũng có thư viện reflection là dart:mirrors.

Tuy nhiên, Flutter vô hiệu hóa (disable) dart:mirrors. Nguyên nhân chính là vì Reflection cản trở quá trình tối ưu hóa code và Tree Shaking (loại bỏ code thừa). Nếu bật Reflection, trình biên dịch không thể biết chắc class/hàm nào có thể bị gọi bằng Reflection, nên nó phải giữ lại toàn bộ code -> dung lượng ứng dụng sau khi build qua lớn và chạy chậm.

Vậy nếu không dùng Reflection lúc runtime, làm sao chúng ta đọc được @Todo hay @JsonKey?

Giải pháp của Dart: Đọc nó ngay lúc viết code (compile-time) và Sinh ra code mới (Code Generation) tự động nhờ build_runner.

Đó là lý do bạn thường xuyên thấy các thư viện Flutter yêu cầu chạy dòng lệnh:

dart run build_runner build # hoặc flutter pub run build_runner build

4. Ví dụ thực tế từ A đến Z: json_serializable

Hãy cùng xem Annotations làm việc như thế nào dựa trên thư viện cực kỳ nổi tiếng là json_serializable. Thư viện này giúp tự động tạo code chuyển đổi từ JSON thành Object Dart.

Bước 4.1: Cấu hình dependencies

Trong file pubspec.yaml, bạn cần khai báo các gói thư viện cần thiết:

  • json_annotation: Chứa các từ khóa (annotations) như @JsonSerializable, @JsonKey.
  • build_runnerjson_serializable: Là công cụ để đọc annotation ở file code của bạn và sinh mã (nằm ở dev_dependencies vì sau khi tạo code xong thì ứng dụng khi chạy sẽ không phụ thuộc trực tiếp vào các package này nữa).
dependencies: json_annotation: ^4.8.1 dev_dependencies: build_runner: ^2.4.6 json_serializable: ^6.7.1

Chạy dart pub get (hoặc flutter pub get).

Bước 4.2: Viết code với Annotations

Tạo file user.dart:

// user.dart import 'package:json_annotation/json_annotation.dart'; // 1. Phải thêm dòng part 'tên_file.g.dart' part 'user.g.dart'; // 2. Đánh dấu class này là cần sinh code map với JSON @JsonSerializable() class User { final String name; // 3. Sử dụng @JsonKey để xử lý trường hợp tên biến Dart // khác với key trong chuỗi JSON (JSON dùng is_active, Dart dùng isActive) @JsonKey(name: 'is_active') final bool isActive; User(this.name, this.isActive); // 4. Khai báo 2 phương thức mặc định của thư viện sẽ tự sinh ra ở file .g.dart factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); Map<String, dynamic> toJson() => _$UserToJson(this); }

Lúc này, trình soạn thảo của bạn (VS Code/Android Studio) có thể sẽ báo lỗi (Error) ở file này. Đừng lo lắng, nguyên nhân là do file user.g.dart và hàm _$UserFromJson chưa tồn tại.

Bước 4.3: Chạy Code Generation với build_runner

Mở Terminal và chạy lệnh:

dart run build_runner build

(Nếu dùng trong dự án Flutter thì gõ flutter pub run build_runner build)

Điều gì xảy ra ẩn bên dưới khi chạy lệnh?

  1. build_runner sẽ quét toàn bộ dự án của bạn.
  2. Nó tìm thấy file user.dart có khai báo part 'user.g.dart'.
  3. Phân tích file (công việc của thư viện analyzer), công cụ này nhận diện class User có mang annotation @JsonSerializable.
  4. Khi phân tích cấu trúc class, nó xác định biến isActive chứa metadata @JsonKey(name: 'is_active').
  5. Nó đưa những thông tin này cho package json_serializable. Package này sẽ dùng thông tin đó viết thành code Dart thuần túy và ghi vào file user.g.dart.

Bước 4.4: Chiêm ngưỡng kết quả (Code sinh ra)

Nhìn vào thư mục cùng cấp của file user.dart, bạn sẽ thấy file mới sinh ra user.g.dart:

// GENERATED CODE - DO NOT MODIFY BY HAND part of 'user.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** User _$UserFromJson(Map<String, dynamic> json) => User( json['name'] as String, json['is_active'] as bool, // Nó nhớ key JSON lấy từ @JsonKey ); Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{ 'name': instance.name, 'is_active': instance.isActive, };

File này đã định nghĩa nội dung thực sự của 2 hàm mà hồi nãy chúng ta viết. Bây giờ lỗi ở file user.dart sẽ biến mất.

Bước 4.5: Sử dụng sau khi build xong

Giờ đây bạn sử dụng đối tượng User y như mọi code Dart bình thường khác:

import 'user.dart'; void main() { String jsonStr = '{"name": "John Doe", "is_active": true}'; Map<String, dynamic> jsonMap = jsonDecode(jsonStr); // Dùng hàm khởi tạo tự sinh User user = User.fromJson(jsonMap); print("User: ${user.name}, Active: ${user.isActive}"); }

5. Tổng kết quy trình Annotations trong Dart

  1. Định nghĩa: Dùng Class có constant constructor (@MyAnnotation).
  2. Khai báo: Đặt nó lên class, thuộc tính hoặc method.
  3. Quét và Phân tích: Công cụ build_runner cùng các thư viện generator sẽ phân tích những vị trí có gán annotation trong quá trình compile-time.
  4. Sinh mã: Nó sẽ tạo file có đuôi .g.dart (g viết tắt cho generated) hoặc .freezed.dart.
  5. Thực thi: Import file .g.dart vào chính file code và sử dụng như code do chính tay mình viết.

Lưu ý: Nếu code trong class User thay đổi (thêm biến, xóa biến…), bạn bắt buộc phải chạy lại lệnh dart run build_runner build để file .g.dart được sinh (update) lại. Bạn cũng có thể dùng cờ --delete-conflicting-outputs để dọn dẹp file rác cũ.

dart run build_runner build --delete-conflicting-outputs
💡

Trong quá trình phát triển (development), nếu không muốn gõ lệnh build liên tục, có thể chuyển từ lệnh build sang watch. Trình build runner sẽ chạy ngầm, mỗi khi bạn lưu (Save) file thì nó tự động chạy sinh code lại ngay lập tức: dart run build_runner watch