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.