Skip to Content
Dart📘 Ngôn ngữ DartThực hành Annotations và Code Generation

Thực hành sử dụng Annotations và Code Generation

Trong bài viết trước, chúng ta đã tìm hiểu mặt lý thuyết đằng sau Annotations và nguyên lý hoạt động của Code Generation trong Dart.

Hôm nay, chúng ta sẽ thực hành viết một Code Generator từ A-Z. Mục tiêu là tạo ra một annotation tên là @Hello(String name), khi gắn nó lên một class bất kỳ, hệ thống sẽ tự sinh ra một biến chứa xâu chào hỏi "Hello [name]!".


1. Kiến trúc của một bộ Code Generator

Để xây dựng một công cụ sinh mã chuẩn mực, bạn thường cần chia mã nguồn làm 3 phần (package) riêng biệt (hoặc 3 bộ file rõ ràng nếu viết chung một project):

  1. Phần Annotations: Chứa định nghĩa các class Annotation (ví dụ: hello_annotation). Package này sẽ được ứng dụng chính import vào phần dependencies.
  2. Phần Generator: Chứa logic thực sự để quét code và sinh mã (ví dụ: hello_generator). Package này dựa vào thư viện source_gen, build và được ứng dụng chính import vào phần dev_dependencies.
  3. Phần Ứng dụng (App): Nơi người dùng cuối import Annotation, viết code và chạy lệnh build_runner.

Trong bài thực hành này, để đơn giản và dễ hiểu nhất, chúng ta sẽ tạo tất cả trong cùng một project Dart nhưng chia ra các file riêng biệt.


Bước 1: Khởi tạo Project & Cài đặt Thư viện

Tạo một project Dart bằng terminal:

dart create practice_gen cd practice_gen

Mở file pubspec.yaml và thay đổi phần dependencies như sau:

name: practice_gen description: A practical guide to code generation. version: 1.0.0 environment: sdk: '>=3.0.0 <4.0.0' # Phần chạy app dependencies: # Không cần thư viện ngoài nào ở đây # Phần sinh mã (Compile time) dev_dependencies: build: ^2.4.1 # Nền tảng build của Dart build_runner: ^2.4.6 # Công cụ chạy các script code gen source_gen: ^1.4.0 # Thư viện hỗ trợ viết Generator dễ dàng hơn

Chạy lệnh dart pub get để tải các thư viện về.


Bước 2: Định nghĩa Annotation

Tạo file lib/hello_annotation.dart:

// lib/hello_annotation.dart /// Class định nghĩa annotation @Hello class Hello { final String name; // Bắt buộc phải là const constructor const Hello(this.name); }

Đây chỉ là một lớp thông thường, đóng vai trò như một “nhãn dán” (label metadata) mang theo dữ liệu (biến name).


Bước 3: Viết Code Generator (Bộ máy sinh mã)

Tạo file lib/hello_generator.dart. Ta sẽ kế thừa class GeneratorForAnnotation<T> từ package source_gen. Class này tự động giúp ta lọc ra chỉ những class nào đã gắn annotation @Hello để xử lý.

// lib/hello_generator.dart import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart'; import 'hello_annotation.dart'; class HelloGenerator extends GeneratorForAnnotation<Hello> { // Hàm này sẽ tự động được gọi mỗi khi tìm thấy 1 class có gắn @Hello @override String generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep) { // 1. Kiểm tra: Chúng ta chỉ muốn gắn @Hello lên một Class if (element is! ClassElement) { throw InvalidGenerationSourceError( '@Hello chỉ có thể sử dụng trên class.', element: element, ); } // 2. Lấy dữ liệu từ file code của người dùng // Lấy giá trị của biến 'name' đã truyền vào @Hello('World') final nameValue = annotation.peek('name')?.stringValue ?? 'Unknown'; // Lấy tên class người dùng đang gắn annotation final className = element.name; // 3. Trả về đoạn code (dưới dạng String) mà ta muốn tự động sinh ra // Ở đây ta sinh ra một biến String toàn cục (global) return ''' // Code tự động sinh ra cho class $className const String hello${className}Message = "Hello, $nameValue!! Welcome to Code Generation!"; '''; } } // Cấu hình Builder (hàm khởi tạo entry point để build_runner gọi tới) Builder helloBuilder(BuilderOptions options) => SharedPartBuilder([HelloGenerator()], 'hello_generator');

Bước 4: Cấu hình build.yaml (Quyền năng của build_runner)

build_runner không tự động biết code generator của bạn nằm ở đâu và áp dụng lên những file nào. Bạn phải khai báo nó với hệ thống thông qua file cấu hình build.yaml.

Tạo file build.yaml ngay tại thư mục gốc của project (cùng cấp với pubspec.yaml):

# build.yaml targets: $default: builders: practice_gen|hello_builder: enabled: true builders: # Tên builder tuỳ ý hello_builder: # Trỏ đến file generator và hàm helloBuilder ở bước 3 import: "package:practice_gen/hello_generator.dart" builder_factories: ["helloBuilder"] build_extensions: { ".dart": [".g.dart"] } auto_apply: dependents build_to: cache applies_builders: ["source_gen|combining_builder"]

Quy tắc build_extensions: { ".dart": [".g.dart"] } nghĩa là: Generator này sẽ đọc file .dart và cứ thế sinh ra file đuôi .g.dart tương ứng.


Bước 5: Sử dụng Thực tế

Giờ chúng ta hãy đóng vai là lập trình viên sử dụng cái mà chúng ta vừa tạo. Mở (hoặc tạo) file lib/main.dart:

// lib/main.dart import 'hello_annotation.dart'; // 1. Bắt buộc: Cần declare part file tự sinh part 'main.g.dart'; // 2. Gắn Annotation @Hello('Bumbii') class MyClass { void doWork() { print('Vào việc thôi...'); } } void main() { MyClass().doWork(); // 3. Sử dụng biến được sinh ra tự động // (Biến này sẽ báo lỗi đỏ lúc chưa chạy build_runner) print(helloMyClassMessage); }

Bước 6: Chạy Build Runner và chiêm ngưỡng thành quả

Mở Terminal tại thư mục gốc của project, gõ lệnh:

dart run build_runner build

Sau vài giây, bạn sẽ thấy tiến trình kết thúc:

[INFO] Succeeded after 1.2s with 1 outputs (2 actions)

Kiểm tra lại thư mục lib, bạn sẽ thấy file main.g.dart vừa được sinh ra, chứa nội dung:

// GENERATED CODE - DO NOT MODIFY BY HAND part of 'main.dart'; // ************************************************************************** // HelloGenerator // ************************************************************************** // Code tự động sinh ra cho class MyClass const String helloMyClassMessage = "Hello, Bumbii!! Welcome to Code Generation!";

Quay lại file main.dart, các lỗi đỏ trước đây đã hoàn toàn biến mất vì hàm helloMyClassMessage nay đã được file .g.dart cung cấp.

Chạy thử ứng dụng:

dart run lib/main.dart

Kết quả:

Vào việc thôi... Hello, Bumbii!! Welcome to Code Generation!

Tổng kết

Bạn đã chính thức tạo thành công hệ thống sinh mã trong Dart với 4 thành phần chủ chốt:

  1. Annotation (@Hello): Thẻ định danh chứa cấu trúc dữ liệu.
  2. Generator (HelloGenerator): Trung tâm logic phân tích cú pháp (dùng source_genanalyzer) để chuyển thể Annotation thành Code String.
  3. build.yaml: Mắt xích kết nối, chỉ đường cho build_runner tìm thấy Generator.
  4. build_runner: Trái tim thực thi tiến trình sinh mã chuẩn hóa của hệ sinh thái Dart/Flutter.

Dù thực tế các bộ generator đình đám như json_serializable hay freezed có hàng nghìn dòng code cực kì phức tạp, nguyên lý hoạt động của chúng vẫn tuân theo đúng 4 thành phần cơ bản ở trên.