📜 ⬆️ ⬇️

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

In the first part, we figured 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 the 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 the named parameter is required, and our code will not compile if the annotated parameter is not specified. We also use @override to indicate that this API defined in the parent class is implemented in a child class. Annotations always begin with the @ 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 easiest things in the Dart language. Earlier it was 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 it defines a const constructor :


 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 the main thing is what we will do with these annotations. This will help us source_gen and build_runner .


How to use build_runner?


build_runner is a Dart package that helps us generate files using Dart code. We will configure Builder files using build.yaml . When it is configured, Builder will be called at every build command or when a file changes. We also have the opportunity to parse the code that has been changed or meets some criteria.


source_gen for understanding Dart code


In a sense, build_runner is a mechanism that answers the question “ When should I generate code?”. At the same time, source_gen answers the question “ What code should be generated?”. source_gen provides a framework that allows Builders to be created for build_runner to work. Also source_gen provides a convenient API for parsing and code generation.


Putting it all together: TODO-report


In the remainder of this article, we will analyze the todo_reporter.dart project, which can be found here .


There is an unwritten rule followed by all projects using code generation: you must 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 todo_reporter.dart directory. Inside this directory, you need to create a todo_reporter directory in which there will be an annotation, a todo_reporter_generator directory for annotation processing, and finally an example directory containing a demonstration of the capabilities of the library being created.


The suffix .dart been added to the root directory name for clarity. Of course, this is not necessary, but I like to follow this rule in order to precisely 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 for Flutter only (like ozzie.flutter ), I would use a different suffix. It’s not necessary, it’s just a naming convention that I try to adhere to.


Creating todo_reporter, our simple annotated package


We are going to create todo_reporter inside todo_reporter.dart . To do this, create a pubspec.yaml file and a lib directory.


pubspec.yaml very 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 are no dependencies, except for the test package used in the development process.


In the lib directory, do the following:



In our case, all we need to add is annotation. Let's create a todo.dart file with our annotation:


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

So that’s all you need for annotation. I said it would be easy. But that is not all. Let's add unit tests to the test directory:


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<AssertionError>())); }); 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 annotations. The code you can find on the link . Now we can go to the generator.


We do a great job: todo_reporter_generator


Now that we know how to create packages, let's create a todo_reporter_generator package. Inside this package should be the pubspec.yaml and build.yaml and the lib directory. The lib directory should contain the src directory and the builder.dart file. Our todo_reporter_generator is considered a separate package that will be added as dev_dependency to other projects. This is done because code generation is only needed during the development phase, and it does not need to be added to the finished application.


pubspec.yaml looks like this:


 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 create build.yaml . This file contains the configuration necessary for our Builders . More details can be read here . build.yaml looks like this:


 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 import property points to the file that the Builder contains, and the builder_factories property points to the methods that will generate the code.


Now we can create the builder.dart file in the lib directory:


 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 todo_reporter_generator.dart file in the src directory:


 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<Todo> { @override FutureOr<String> generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep) { return "// Hey! Annotation found!"; } } 

As you can see, in the builder.dart file we defined the todoReporter method that the Builder creates. Builder is created using SharedPartBuilder , which is used by our TodoReporterGenerator . So build_runner and source_gen work together.


Our TodoReporterGenerator is a subclass of GeneratorForAnnotation , so the generateForAnnotatedElement method will be executed only when this annotation ( @Todo in our case) is found in the code.


The generateForAnnotatedElement method 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 because it allows you to avoid mistakes in the future.


Thus, each time code is generated, our todo_repoter_generator will create a part file, with a comment // Hey! Annotation found! // Hey! Annotation found! In the next article, we will learn how to handle annotations.


Putting it all together: using todo_reporter


Now you can demonstrate how todo_reporter.dart . It is a good practice to add an example project when working with packages. So other developers can 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 example directory and add 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, let's start our generator:


 $ flutter packages pub run build_runner build 

After the 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 have achieved what we wanted! Now we can generate the correct Dart file for each @Todo annotation 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 some really cool things. After all, the code that is generated now does not make much sense.


')

Source: https://habr.com/ru/post/446264/


All Articles