In previous lessons, we learned about mixins, mastering advanced techniques for flexible code reuse in object-oriented programming. Today we'll explore another powerful feature in Dart — metadata and reflection. They give code the ability to "self-describe" and "dynamically analyze," playing important roles in framework development, code generation, serialization, and other scenarios.
I. Metadata Annotations: Adding "Extra Information" to Code
Metadata is additional information embedded in code that describes the code itself. It doesn't directly affect code execution logic but can be read by compilers, tools, or runtime environments for code checking, generation, or dynamic processing.
In Dart, metadata is represented by annotations, which start with the @ symbol and are typically placed before code elements (classes, methods, variables, etc.).
1. Built-in Metadata Annotations
Dart provides several commonly used built-in annotations:
(1) @override: Marking method overrides
Explicitly indicates that a method overrides a parent class method, helping the compiler check for correct overriding (e.g., matching method names and parameters):
class Animal {
void makeSound() => print("Animal sound");
}
class Dog extends Animal {
// Explicitly mark method override
@override
void makeSound() => print("Woof woof");
}
void main() {
Dog().makeSound(); // Output: Woof woof
}
If you accidentally misspell the method name (like writing makeSound2), the compiler will show an error, preventing simple mistakes.
(2) @deprecated: Marking obsolete code
Marks code that is no longer recommended for use. The compiler will issue warnings where this code is used:
class Tool {
// Mark this method as obsolete
@deprecated
void oldMethod() => print("This is an old method, deprecated");
void newMethod() => print("This is the new method, recommended for use");
}
void main() {
final tool = Tool();
tool.oldMethod(); // Compile-time warning: 'oldMethod' is deprecated
tool.newMethod(); // No warning
}
You can usually add a description after the annotation to tell users about alternatives:
@deprecated
/// Recommended: use newMethod() instead
void oldMethod() => print("This is an old method, deprecated");
(3) @pragma: Passing specific instructions to the compiler
Used to pass platform-specific instructions to the Dart compiler, such as disabling specific warnings:
// Disable "unused variable" warning
@pragma('vm:unused')
int unusedVariable = 10;
void main() {
// Variable is unused but won't trigger a warning
}
2. The Nature of Metadata: Special Class Instances
All annotations in Dart are essentially class instances. Built-in annotations like @override and @deprecated are actually predefined classes. We can understand this through source code:
// Simplified implementation of @deprecated
class deprecated {
final String? message;
const deprecated([this.message]);
}
// When used, @deprecated is equivalent to @deprecated()
@deprecated // Equivalent to @deprecated()
void oldMethod() {}
This is why annotations can take parameters (like @deprecated("Use newMethod instead")).
II. Custom Annotations: Creating Your Own Metadata
In addition to built-in annotations, we can define our own annotations to describe business-specific information (such as serialization configurations, permission checks, etc.).
1. Defining Custom Annotations
Custom annotations are essentially classes with const constructors (since annotations need to have determined values at compile time):
// Define a "logging annotation": marks methods that need logging
class Logged {
final bool printParams; // Whether to print parameters
final bool printResult; // Whether to print return value
// Must be a const constructor (annotations are parsed at compile time)
const Logged({this.printParams = false, this.printResult = false});
}
// Define a "permission annotation": marks methods that require permission checks
class RequirePermission {
final String permission;
const RequirePermission(this.permission);
}
2. Using Custom Annotations
Apply annotations to elements like classes, methods, and variables:
class UserService {
// Use @Logged annotation, specifying to print parameters and return value
@Logged(printParams: true, printResult: true)
String getUserInfo(int id) {
return "Information for user $id";
}
// Use @RequirePermission annotation, specifying "admin" permission is needed
@RequirePermission("admin")
void deleteUser(int id) {
print("Deleting user $id");
}
}
At this point, annotations are just "markers" without actual functionality. They need to work with annotation processors to take effect.
3. Compile-time Annotation Processing: Code Generation
The value of custom annotations is typically realized through compile-time code generation: during the code compilation phase, tools read annotation information and automatically generate auxiliary code (such as serialization logic, logging code, etc.).
Take the json_serializable package as an example (it essentially implements automatic JSON serialization code generation through the custom @JsonSerializable annotation):
- Define a model class and add the annotation:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // Generated code file
@JsonSerializable() // Custom annotation
class User {
final String name;
final int age;
User({required this.name, required this.age});
// Generated serialization methods
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
2 . Run the code generation command:
dart run build_runner build
3 . The tool reads the @JsonSerializable annotation and automatically generates the user.g.dart file containing serialization logic.
The advantage of this "compile-time processing" is that it doesn't affect runtime performance (code is generated in advance) and supports environments like Flutter where reflection is disabled.
III. Reflection: Allowing Code to "Understand Itself"
Reflection refers to a program's ability to dynamically obtain its own structure (such as class, method, and annotation information) and manipulate it at runtime. Simply put, it's code that can "observe" and "modify" itself.
Reflection in Dart is mainly implemented through the dart:mirrors library, but this library is disabled in Flutter (as it increases package size and affects performance). In actual development, the reflectable package is more commonly used (a lightweight reflection library that supports compile-time configuration).
1. Basic Concepts of Reflection
Reflection can achieve the following:
- Obtain information about class names, methods, properties, etc.
- Dynamically create class instances
- Dynamically call methods or modify property values
- Read and process annotation information
2. Implementing Reflection with the reflectable Package
Since dart:mirrors isn't available in Flutter, we'll demonstrate reflection using the reflectable package.
(1) Adding Dependencies
Add to pubspec.yaml:
dependencies:
reflectable: ^4.0.13
dev_dependencies:
build_runner: ^2.4.4
(2) Defining a Reflection Configuration Class
Create a class inheriting from Reflectable, specifying reflection capabilities needed (such as obtaining methods, reading annotations, etc.):
// reflector.dart
import 'package:reflectable/reflectable.dart';
// Configure reflection capabilities: support instantiation, method invocation, annotation reading
class MyReflector extends Reflectable {
const MyReflector()
: super(
instanceInvokeCapability, // Allow calling instance methods
typeAnnotationQuantifyCapability, // Allow reading type annotations
declarationsCapability, // Allow obtaining class declarations (methods, properties, etc.)
);
}
// Create reflector instance
const myReflector = MyReflector();
(3) Marking Classes for Reflection
Add the @myReflector annotation to classes that need reflection:
import 'reflector.dart';
// Mark this class with the reflector to enable reflection
@myReflector
class UserService {
@Logged(printParams: true, printResult: true)
String getUserInfo(int id) {
return "Information for user $id";
}
@RequirePermission("admin")
void deleteUser(int id) {
print("Deleting user $id");
}
}
(4) Generating Reflection Code
Run the command to generate auxiliary code needed for reflection:
dart run build_runner build
This will generate a reflectable.g.dart file containing metadata needed for reflection.
(5) Using Reflection to Obtain Class Information and Call Methods
import 'package:reflectable/reflectable.dart';
import 'reflector.dart';
import 'reflectable.g.dart'; // Import generated code
void main() {
// Initialize reflection
initializeReflectable();
// Create UserService instance
final userService = UserService();
// Get reflection information for the class
final instanceMirror = myReflector.reflect(userService);
final classMirror = instanceMirror.type;
// Print class name
print(
"Class name: ${classMirror.simpleName}",
); // Output: Class name: UserService
// Iterate through class methods
print("\nMethod list:");
for (var method in classMirror.declarations.values) {
if (method is MethodMirror && !method.isStatic) {
print("- ${method.simpleName}");
// Check if method has @Logged annotation
for (var annotation in method.annotations) {
if (annotation is Logged) {
print(
" Has @Logged annotation: printParams=${annotation.printParams}",
);
}
}
}
}
// Dynamically call getUserInfo method
print("\nDynamically calling method:");
final result = instanceMirror.invoke("getUserInfo", [100]);
print(
"Call result: $result",
); // Output: Call result: Information for user 100
}
Output result:
Class name: UserService
Method list:
- getUserInfo
Has @Logged annotation: printParams=true
- deleteUser
Dynamically calling method:
Call result: Information for user 100
3. Scenarios Where Reflection Should Be Used Cautiously
Although reflection is powerful, it should be used cautiously in actual development (especially in Flutter projects) for the following reasons:
- Performance overhead: Reflection requires parsing class information at runtime and is much slower than direct method calls (usually 10-100 times slower).
- Flutter incompatibility: The dart:mirrors library is disabled in Flutter. While reflectable can be used, it increases package size.
- Poor code readability: Dynamic method calls make code logic more obscure and difficult to debug.
- Broken type safety: Reflection can bypass compiler type checks, easily leading to runtime errors.
Alternative: Prefer compile-time code generation (like json_serializable, freezed), which generates code during compilation, balancing flexibility and performance.
IV. Practical Application Scenarios for Metadata and Reflection
1. Serialization/Deserialization
Packages like json_serializable use the @JsonSerializable annotation to generate JSON conversion code at compile time, avoiding the need to manually write tedious serialization logic.
2. Dependency Injection Frameworks
Dependency injection frameworks like get_it automatically create objects and inject dependencies through reflection (or code generation), simplifying object management.
3. Routing Management
Flutter routing frameworks (like auto_route) mark pages with the @MaterialRoute annotation and generate route tables at compile time, enabling type-safe route navigation.
4. Logging and Event Tracking
By marking methods that need event tracking with custom annotations (like @LogEvent), combined with compile-time generation or reflection, logging logic can be automatically added, reducing repetitive code.
Top comments (0)