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):
- 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ầndependencies. - 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ệnsource_gen,buildvà được ứng dụng chính import vào phầndev_dependencies. - 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_genMở 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ơnChạ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 buildSau 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.dartKế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:
- Annotation (
@Hello): Thẻ định danh chứa cấu trúc dữ liệu. - Generator (
HelloGenerator): Trung tâm logic phân tích cú pháp (dùngsource_genvàanalyzer) để chuyển thể Annotation thành Code String. - build.yaml: Mắt xích kết nối, chỉ đường cho
build_runnertìm thấy Generator. - 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.