[Part 2] Code generation in Dart: Annotations, source_gen and build_runner

Jorge Coca
Flutter Community
Published in
7 min readOct 31, 2018

In “[Part 1] Code generation in Dart: the basics”, we covered what’s the motivation behind code generation, and listed the most important tools that we have available in Dart to let a computer do the hard work for us. In this article, we are going to cover how to create and use Dart annotations, and how to use source_gen and build_runner to start processing those annotations.

Annotations in Dart

An annotation is a form of representing syntactic metadata that can be added to our Dart code; in other words, a way of adding extra information to any component in our code, such as class or a method. Annotations are used everywhere in our Dart code: we use @required to specify that a named parameter is not optional, so our code won’t compile if the field annotated is not present, or we use @override to identify those APIs given by a parent class that are implemented in a child class. How do we know they are annotations? Well, they are easy to find, since they are prefixed by @.

But how do we create our annotations?

Although the idea of having “metadata” in our classes sounds very exotic and complex, the truth is that annotations are one of the simplest things to do in Dart. In the paragraph above I mentioned that annotations just carry over extra information. They are like data classes. PODOs… (Plain Old Dart Objects). Any class can be transformed into an annotation, as long as they provide a const constructor:

class Todo {
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 pretty simple. What matters is what we do with those annotations; the information that the annotation contains and how we use it is what makes them special. And that’s where source_gen and build_runner will help us.

How should we use build_runner?

build_runner is a Dart package that will help us to generate files using Dart code. We will configure Builder files, via a build.yaml; once it is configured, we will receive updates once a build is triggered, or a file has changed, and we will be able to parse the code that changed or meets a certain criteria.

source_gen to make sense of the Dart code

In a way, you can think of build_runner as the mechanism that answers the question of when you will need to generate code, while source_gen answers the question of what code needs to be generated. source_gen offers a framework to build the Builders that build_runner expects, while exposing a friendly API to parse and generate code.

Putting all the pieces together: a TODO reporter

For the rest of the article, we are going to work on a pet project called todo_reporter.dart that you can find in this link.

This is a non written rule that you can find in all the projects that use code generation: you will create a package for your annotations, and a different package for the generator that adds value to those annotations. All the information you need to find to create a library package in Dart/Flutter can be found in this link.

So what we are going to do is to create a folder, that I will name todo_reporter.dart . Inside this folder, I will add my todo_reporter that will contain the annotations, todo_reporter_generator for processing the code, and finally an example package to demo the capabilities of my library.

The reason why I suffixed the root folder with .dart is for clarity; while it is not mandatory, I like to follow that to make clear that this package could be used in any Dart project. On the contrary, if I wanted to mark this package as Flutter only, like ozzie.flutter, then I will use a different suffix. This is not mandatory to do, just a naming convention that I like to follow.

Creating todo_reporter, our annotations package, and the simplest one

We are going to create our todo_reporter inside todo_reporter.dart , add the pubspec.yaml and the the lib folder. The pubspec is really simple:

name: todo_reporter
description: Keep track of all your TODOs.
version: 1.0.0
author: Jorge Coca <jcocaramos@gmail.com>
homepage: https://github.com/jorgecoca/todo_reporter.dart
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies: dev_dependencies:
test: 1.3.4

There’s no real dependencies other than the test package, only used for development purposes.

Inside the lib folder, we are going to do the following:

  • We are going to create a todo_reporter.dart , and we will register in there all the classes that expose the public API of our package using export . This is a good practice to follow, since any public class in our package could be imported by adding import "package:todo_reporter/todo_reporter.dart . You can see how this class looks here: https://github.com/jorgecoca/todo_reporter.dart/blob/master/todo_reporter/lib/todo_reporter.dart
  • Inside the lib folder, we are now going to create a src folder that will contain all the code, public or non public.

In our case, the only thing that we need to include is the annotation. Let’s create a todo.dart file inside with this content:

class Todo {  
final String name;
final String todoUrl;
const Todo(this.name, {this.todoUrl}) : assert(name != null);
}

Well, this is all we need for our annotation. I promised this would be easy, right? Well, we are not done yet. Let’s add some unit tests in the test package:

This is all we need for creating annotations. You can find the code in this link.

Let’s work now on code generation.

Doing the cool work: todo_reporter_generator

Now that we know how to create packages, let’s create one called todo_reporter_generator . Inside it, you should find a pubspec.yaml, a build.yaml file, a lib folder, and inside the lib folder, a src folder and a todo_reporter_generator.dart file where we will include our export statements. Our todo_reporter_generator is considered a different package that will be added as a dev_dependency to other projects. This makes sense, since we only care about code generation during the development process, and this does not to be included in a production bundle.

Let’s take a look at how our pubspec.yaml should look:

name: todo_reporter_generator
description: An annotation processor for @Todo annotations.
version: 1.0.0
author: Jorge Coca <jcocaramos@gmail.com>
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 complete the build.yaml . This file will contain the configuration needed for your Builders . You can find more information here: https://github.com/dart-lang/build/blob/master/build_config/README.md

Our build.yaml will look like this at this moment:

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"]

Our import entry should point to the file that will contain our Builder , and the builder_factories entry should point to those methods that will build the code.

Let’s go ahead and create these files then: let’s create a builder.dart file inside lib , and inside src let’s add a file called todo_reporter_generator.dart with the following contents:

As we can see, in builder.dart we have a todoReporter method that will create a Builder ; this Builder is provided by using a SharedPartBuilder that takes our TodoReporterGenerator .That’s how build_runner and source_gen work together.

Our TodoReporterGenerator is of type GeneratorForAnnotation ; that is, it will only execute generateForAnnotatedElement when it finds a piece of code that has been annotated with the given annotation, which is Todo in our case.

The return value for generateForAnnotatedElement is a String value that will contain our generated code; if the code generated does not compile, our build phase will fail, which is pretty neat when avoiding bugs.

With these files in our todo_repoter_generator project, every time when attempt to auto generate code, it will create a part file with a comment that says // Hey! Annotation found! . We will learn in the next article how to process annotations 😉

Putting all the pieces together: using our todo_reporter

The last piece to start using our todo_repoter.dart is to demonstrate its capabilities on a project. It is a good practice to add an example project when working on packages, so other developers can see how the APIs are used on a real world project.

Let’s go ahead and create a project and add the needed dependencies in the pubspec.yaml file; in my case, I just created a Flutter project inside the example folder, and added these 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/

Now, after getting the packages (`flutter packages get`), we use our annotations:

import 'package:todo_reporter/todo_reporter.dart';

@Todo('Complete implementation of TestClass')
class TestClass {}

With all these pieces in place, let’s go ahead and run our generator:

$ flutter packages pub run build_runner build

Once it finishes executing this command, you will notice a new file on your project: todo.g.dart with the following content:

// GENERATED CODE - DO NOT MODIFY BY HANDpart of 'todo.dart';// *****************************************************************
// TodoReporterGenerator
// ********************************************************************
// Hey! Annotation found!

Success! We have completed our task! Now we can generate a valid Dart file for every Todo annotation found in our code. Give it a try and feel free to create as many as you want!

In the next article…

Now that we have the proper setup to generate files, in the next article we will learn how to utilize our annotations, so our generate code can actually do cool things; after all, the code that we are generating at the moment has no purpose.

You can follow me on https://twitter.com/jcocaramos and see more code here on my public Github https://github.com/jorgecoca

--

--

Jorge Coca
Flutter Community

Android engineer at @bmwna. Born in Madrid, living in Chicago. I have watched La La Land more times than you… and I love singing and dancing in public xD