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

Jorge Coca
Oct 31, 2018 · 7 min read

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 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 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 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 and will help us.

How should we use ?

is a Dart package that will help us to generate files using Dart code. We will configure files, via a 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 as the mechanism that answers the question of when you will need to generate code, while answers the question of what code needs to be generated. offers a framework to build the Builders that 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 . Inside this folder, I will add my that will contain the annotations, for processing the code, and finally an package to demo the capabilities of my library.

The reason why I suffixed the root folder with 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 , our annotations package, and the simplest one

We are going to create our inside , add the and the the 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
sdk: ">=2.0.0 <3.0.0"
dependencies: dev_dependencies:
test: 1.3.4

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

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

  • We are going to create a , and we will register in there all the classes that expose the public API of our package using . This is a good practice to follow, since any public class in our package could be imported by adding . 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 folder, we are now going to create a 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 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 . Inside it, you should find a , a file, a folder, and inside the folder, a folder and a file where we will include our statements. Our is considered a different package that will be added as a 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 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
sdk: ">=2.0.0 <3.0.0"
build: '>=0.12.0 <2.0.0'
source_gen: ^0.9.0
path: ../todo_reporter/
build_test: ^0.10.0
build_runner: '>=0.9.0 <0.11.0'
test: ^1.0.0

Now, let’s complete the . This file will contain the configuration needed for your . You can find more information here: https://github.com/dart-lang/build/blob/master/build_config/README.md

Our will look like this at this moment:

enabled: true

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 entry should point to the file that will contain our , and the entry should point to those methods that will build the code.

Let’s go ahead and create these files then: let’s create a file inside , and inside let’s add a file called with the following contents:

As we can see, in we have a method that will create a ; this is provided by using a that takes our .That’s how and work together.

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

The return value for 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 project, every time when attempt to auto generate code, it will create a file with a comment that says . 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 is to demonstrate its capabilities on a project. It is a good practice to add an 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 file; in my case, I just created a Flutter project inside the example folder, and added these dependencies:

sdk: flutter
path: ../todo_reporter/

build_runner: 1.0.0
sdk: flutter
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: 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 file for every 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

Flutter Community

Articles and Stories from the Flutter Community