Code Generation in Dart. Part 2. Annotations, source_gen and build_runner

Original author: Jorge Coca
  • 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 @requiredto indicate that a named parameter is required, and our code will not compile if the annotated parameter is not specified. We also use @overrideto 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 constconstructor 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_genand 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 Builderfiles using build.yaml. When it is configured, it Builderwill be called upon every command buildor 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_runnerthis is a mechanism that answers the question " When do I need to generate code?" At the same time, it source_genanswers the question “ What code should be generated?”. source_genprovides a framework for creating Builders to work build_runner. It also source_genprovides 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_reporterin which the annotation will be located, a directory todo_reporter_generatorfor processing annotations and, finally, a directory examplecontaining a demonstration of the capabilities of the library being created.


A suffix has .dartbeen 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_reporterinside todo_reporter.dart. To do this, create a file pubspec.yamland 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 testused in the development process.


In the directory libyou need to do the following:


  • It is necessary to create a file todo_reporter.dartin which, using export, all classes having a public API will be indicated. This is good practice, since any class in our package can be imported using import 'package:todo_reporter/todo_reporter.dart';. You can see this class here .
  • Inside the directory, libwe will create a directory srccontaining all the code - public and non-public.

In our case, all we need to add is an annotation. Let's create a file todo.dartwith 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:


todo_test.dart
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.yamland build.yamland directory lib. The directory libmust have a directory srcand a file builder.dart. Ours todo_reporter_generatoris considered a separate package, which will be added as dev_dependencyto 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.yamlas 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 importpoints to the file that is contained Builder, and the property builder_factoriespoints to the methods that will generate the code.


Now we can create the file builder.dartin 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.dartin 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.dartwe defined the method todoReporterthat creates Builder. Buildercreated with the help SharedPartBuilderthat uses ours TodoReporterGenerator. So build_runnerthey source_genwork together.


Ours TodoReporterGeneratoris a subclass GeneratorForAnnotation, so the method generateForAnnotatedElementwill only be executed when the given annotation ( @Todoin our case) is found in the code.


The method generateForAnnotatedElementreturns 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_generatorwill create a partfile 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 exampleand 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 @Todoin 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.


Also popular now: