Annotations
An annotation is metadata—data about data. An annotation is a way to keep additional information about the code in the code itself. An annotation can have parameter values to pass specific information about an annotated member. An annotation without parameters is called a marker annotation. The purpose of a marker annotation is just to mark the annotated member.
Dart annotations are constant expressions beginning with the @
character. We can apply annotations to all the members of the Dart language, excluding comments and annotations themselves. Annotations can be:
- Interpreted statically by parsing the program and evaluating the constants via a suitable interpreter
- Retrieved via reflection at runtime by a framework
Note
The documentation generator does not add annotations to the generated documentation pages automatically, so the information about annotations must be specified separately in comments.
Built-in annotations
There are several built-in annotations defined in the Dart SDK interpreted by the static analyzer. Let's take a look at them.
Deprecated
The first built-in annotation is deprecated
, which is very useful when you need to mark a function, variable, a method of a class, or even a whole class as deprecated and that it should no longer be used. The static analyzer generates a warning whenever a marked statement is used in code, as shown in the following screenshot:
Override
Another built-in annotation is override
. This annotation informs the static analyzer that any instance member, such as a method, getter, or setter, is meant to override the member of a superclass with the same name. The class instance variables as well as static members never override each other. If an instance member marked with override fails to correctly override a member in one of its superclasses, the static analyzer generates the following warning:
Proxy
The last annotation is proxy
. Proxy is a well-known pattern used when we need to call a real class's methods through the instance of another class. Let's assume that we have the following Car
class:
part of cars; // Class Car class Car { int _speed = 0; // The car speed int get speed => _speed; // Accelerate car accelerate(acc) { _speed += acc; } }
To drive the car instance, we must accelerate it as follows:
library cars; part 'car.dart'; main() { Car car = new Car(); car.accelerate(10); print('Car speed is ${car.speed}'); }
We now run our example to get the following result:
Car speed is 10
In practice, we may have a lot of different car types and would want to test all of them. To help us with this, we created the CarProxy
class by passing an instance of Car
in the proxy's constructor. From now on, we can invoke the car's methods through the proxy and save the results in a log as follows:
part of cars; // Proxy to [Car] class CarProxy { final Car _car; // Create new proxy to [car] CarProxy(this._car); @override noSuchMethod(Invocation invocation) { if (invocation.isMethod && invocation.memberName == const Symbol('accelerate')) { // Get acceleration value var acc = invocation.positionalArguments[0]; // Log info print("LOG: Accelerate car with ${acc}"); // Call original method _car.accelerate(acc); } else if (invocation.isGetter && invocation.memberName == const Symbol('speed')) { var speed = _car.speed; // Log info print("LOG: The car speed ${speed}"); return speed; } return super.noSuchMethod(invocation); } }
As you can see, CarProxy
does not implement the Car
interface. All the magic happens inside noSuchMethod
, which is overridden from the Object
class. In this method, we compare the invoked member name with accelerate
and speed
. If the comparison results match one of our conditions, we log the information and then call the original method on the real object. Now let's make changes to the main
method, as shown in the following screenshot:
Here, the static analyzer alerts you with a warning because the CarProxy
class doesn't have the accelerate
method and the speed
getter. You must add the proxy
annotation to the definition of the CarProxy
class to suppress the static analyzer warning, as shown in the following screenshot:
Now with all the warnings gone, we can run our example to get the following successful result:
Car speed is 10 LOG: Accelerate car with 10 LOG: The car speed 20 Car speed through proxy is 20
Custom annotations
Let's say we want to create a test framework. For this, we will need several custom annotations to mark methods in a testable class to be included in a test case. The following code has two custom annotations. In the case, where we need only marker annotation, we use a constant string test. In the event that we need to pass parameters to an annotation, we will use a Test
class with a constant constructor, as shown in the following code:
library test; // Marker annotation test const String test = "test"; // Test annotation class Test { // Should test be ignored? final bool include; // Default constant constructor const Test({this.include:true}); String toString() => 'test'; }
The Test
class has the final include
variable initialized with a default value of true
. To exclude a method from tests, we should pass false
as a parameter for the annotation, as shown in the following code:
library test.case; import 'test.dart'; import 'engine.dart'; // Test case of Engine class TestCase { Engine engine = new Engine(); // Start engine @test testStart() { engine.start(); if (!engine.started) throw new Exception("Engine must start"); } // Stop engine @Test() testStop() { engine.stop(); if (engine.started) throw new Exception("Engine must stop"); } // Warm up engine @Test(include:false) testWarmUp() { // ... } }
In this scenario, we test the Engine
class via the invocation of the testStart
and testStop
methods of TestCase
, while avoiding the invocation of the testWarmUp
method.
So what's next? How can we really use annotations? Annotations are useful with reflection at runtime, so now it's time to discuss how to make annotations available through reflection.