Bỏ qua để đến nội dung

Thực hành 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]!".


Để 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

Phần tiêu đề “Bước 1: Khởi tạo Project & Cài đặt Thư viện”

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

Terminal window
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ề.


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ã)

Phần tiêu đề “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)

Phần tiêu đề “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.


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ả

Phần tiêu đề “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:

Terminal window
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:

Terminal window
dart run lib/main.dart

Kết quả:

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

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.