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.
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.
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
.
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.
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.
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.
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:
todo_reporter.dart
, in which, using the export
, all classes that have a public API will be listed. This is good practice, as any class in our package can be imported using import 'package:todo_reporter/todo_reporter.dart';
. You can see this class here .lib
directory we will create a src
directory containing all the code - public and non-public.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:
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.
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.
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.
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