Code Generation in Dart. Part 2. Annotations, source_gen and build_runner
- Transfer
In the first part, we found out why code generation is needed and listed the necessary tools for code generation in Dart. In the second part, we will learn how to create and use annotations in Dart, as well as how to use source_gen and build_runner to start code generation.
Dart annotations
Annotations are syntactic metadata that can be added to code. In other words, it is an opportunity to add additional information to any component of the code, for example, to a class or method. Annotations are widely used in Dart code: we use @required
to indicate that a named parameter is required, and our code will not compile if the annotated parameter is not specified. We also use @override
to indicate that a given API defined in a parent class is implemented in a child class. Annotations always begin with a symbol @
.
How to create your own annotation?
Although the idea of adding metadata to the code sounds a bit exotic and complicated, annotations are one of the simplest things in the Dart language. It was previously said that annotations simply carry additional information . They are similar to PODO (Plain Old Dart Objects). And any class can serve as an annotation if a const
constructor is defined in it :
class {
final String name;
final String todoUrl;
const Todo(this.name, {this.todoUrl}) : assert(name != null);
}
@Todo('hello first annotation', todoUrl: 'https://www.google.com')
class HelloAnnotations {}
As you can see, annotations are very simple. And what matters is what we will do with these annotations. This will help us source_gen
and build_runner
.
How to use build_runner?
build_runner
- This is a Dart package that will help us generate files using Dart code. We will configure the Builder
files using build.yaml
. When it is configured, it Builder
will be called upon every command build
or when the file is changed. We also have the opportunity to parse code that has been modified or meets certain criteria.
source_gen for understanding Dart code
In a sense, build_runner
this is a mechanism that answers the question " When do I need to generate code?" At the same time, it source_gen
answers the question “ What code should be generated?”. source_gen
provides a framework for creating Builders to work build_runner
. It also source_gen
provides a convenient API for parsing and generating code.
Putting it all together: TODO report
In the remainder of this article, we will take apart the todo_reporter.dart project , which can be found here .
There is an unwritten rule that all projects using code generation follow: you need to create a package containing annotations , and a separate package for the generator that uses these annotations. Information on how to create a package library in Dart / Flutter can be found here .
First you need to create a directory todo_reporter.dart
. Inside this directory you need to create a directory todo_reporter
in which the annotation will be located, a directory todo_reporter_generator
for processing annotations and, finally, a directory example
containing a demonstration of the capabilities of the library being created.
A suffix has .dart
been added to the root directory name for clarity. Of course, this is not necessary, but I like to follow this rule to accurately indicate the fact that this package can be used in any Dart project. On the contrary, if I wanted to indicate that this package is only for Flutter (like ozzie.flutter ), I would use a different suffix. This is not necessary, it is just a naming convention that I try to adhere to.
Creating todo_reporter, our simple annotation package
We are going to create todo_reporter
inside todo_reporter.dart
. To do this, create a file pubspec.yaml
and directory lib
.
pubspec.yaml
very simple:
name: todo_reporter
description: Keep track of all your TODOs.
version: 1.0.0
author: Jorge Coca
homepage: https://github.com/jorgecoca/todo_reporter.dart
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
dev_dependencies:
test: 1.3.4
There are no dependencies, except for the package test
used in the development process.
In the directory lib
you need to do the following:
- It is necessary to create a file
todo_reporter.dart
in which, usingexport
, all classes having a public API will be indicated. This is good practice, since any class in our package can be imported usingimport 'package:todo_reporter/todo_reporter.dart';
. You can see this class here . - Inside the directory,
lib
we will create a directorysrc
containing all the code - public and non-public.
In our case, all we need to add is an annotation. Let's create a file todo.dart
with our annotation:
class Todo {
final String name;
final String todoUrl;
const Todo(this.name, {this.todoUrl}) : assert(name != null);
}
So that’s all it takes to annotate. I said that it will be simple. But that is not all. Let's add unit tests to the directory test
:
import 'package:test/test.dart';
import 'package:todo_reporter/todo_reporter.dart';
void main() {
group('Todo annotation', () {
test('must have a non-null name', () {
expect(() => Todo(null), throwsA(TypeMatcher()));
});
test('does not need to have a todoUrl', () {
final todo = Todo('name');
expect(todo.todoUrl, null);
});
test('if it is a given a todoUrl, it will be part of the model', () {
final givenUrl = 'http://url.com';
final todo = Todo('name', todoUrl: givenUrl);
expect(todo.todoUrl, givenUrl);
});
});
}
This is all we need to create the annotation. You can find the code here . Now we can go to the generator.
Making a cool job: todo_reporter_generator
Now that we know how to create packages, let's create a package todo_reporter_generator
. Inside this package should be the files pubspec.yaml
and build.yaml
and directory lib
. The directory lib
must have a directory src
and a file builder.dart
. Ours todo_reporter_generator
is considered a separate package, which will be added as dev_dependency
to other projects. This is because code generation is needed only at the development stage, and it does not need to be added to the finished application.
pubspec.yaml
as follows:
name: todo_reporter_generator
description: An annotation processor for @Todo annotations.
version: 1.0.0
author: Jorge Coca
homepage: https://github.com/jorgecoca/todo_reporter.dart
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
build: '>=0.12.0 <2.0.0'
source_gen: ^0.9.0
todo_reporter:
path: ../todo_reporter/
dev_dependencies:
build_test: ^0.10.0
build_runner: '>=0.9.0 <0.11.0'
test: ^1.0.0
Now let's create build.yaml
. This file contains the configuration necessary for our Builders . More details can be found here . build.yaml
as follows:
targets:
$default:
builders:
todo_reporter_generator|todo_reporter:
enabled: true
builders:
todo_reporter:
target: ":todo_reporter_generator"
import: "package:todo_reporter_generator/builder.dart"
builder_factories: ["todoReporter"]
build_extensions: {".dart": [".todo_reporter.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
The property import
points to the file that is contained Builder
, and the property builder_factories
points to the methods that will generate the code.
Now we can create the file builder.dart
in the directory lib
:
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:todo_reporter_generator/src/todo_reporter_generator.dart';
Builder todoReporter(BuilderOptions options) =>
SharedPartBuilder([TodoReporterGenerator()], 'todo_reporter');
And the file todo_reporter_generator.dart
in the directory src
:
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:source_gen/source_gen.dart';
import 'package:todo_reporter/todo_reporter.dart';
class TodoReporterGenerator extends GeneratorForAnnotation {
@override
FutureOr generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
return "// Hey! Annotation found!";
}
}
As you can see, in the file builder.dart
we defined the method todoReporter
that creates Builder
. Builder
created with the help SharedPartBuilder
that uses ours TodoReporterGenerator
. So build_runner
they source_gen
work together.
Ours TodoReporterGenerator
is a subclass GeneratorForAnnotation
, so the method generateForAnnotatedElement
will only be executed when the given annotation ( @Todo
in our case) is found in the code.
The method generateForAnnotatedElement
returns a string containing our generated code. If the generated code does not compile, then the entire build phase will fail . This is very useful as it avoids errors in the future.
Thus, with each code generation, ours todo_repoter_generator
will create a part
file with a comment. // Hey! Annotation found!
In the next article, we will learn how to process annotations.
Putting it all together: using todo_reporter
Now you can demonstrate the work todo_reporter.dart
. It is good practice - Add example
-project when working with packages. So other developers will be able to see how the API can be used in a real project.
Let's create a project and add the required dependencies to pubspec.yaml
. In our case, we will create a Flutter project inside the directory example
and add the dependencies:
dependencies:
flutter:
sdk: flutter
todo_reporter:
path: ../todo_reporter/
dev_dependencies:
build_runner: 1.0.0
flutter_test:
sdk: flutter
todo_reporter_generator:
path: ../todo_reporter_generator/
After receiving the packages ( flutter packages get
) we can use our annotation:
import 'package:todo_reporter/todo_reporter.dart';
@Todo('Complete implementation of TestClass')
class TestClass {}
Now that everything is in place, run our generator:
$ flutter packages pub run build_runner build
Upon completion of the command, you will notice a new file in our project: todo.g.dart
. It will contain the following:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'todo.dart';
// *****************************************************************
// TodoReporterGenerator
// ********************************************************************
// Hey! Annotation found!
We achieved what we wanted! Now we can generate the correct Dart file for each annotation @Todo
in our code. Try and create as many as you need.
In the next article
Now we have the correct settings for generating files. In the next article, we will learn how to use annotations so that the generated code can do really cool things. After all, the code that is being generated now does not make much sense.