В первой части мы выяснили зачем нужна кодогенерация и перечислили необходимые инструменты для кодогенерации в Dart. Во второй части мы узнаем как создавать и использовать аннотации в Dart, а также как использовать source_gen и build_runner, чтобы для запуска кодогенерации.

Аннотации в Dart
Аннотации — это синтаксические метаданные, которые могут быть добавлены к коду. Другими словами, это возможность добавить дополнительную информацию к любому компоненту кода, например, к классу или методу. Аннотации широко используются в Dart-коде: мы используем @required, чтобы указать, что именованный параметр является обязательным, и наш код не скомпилируется, если аннотированный параметр не указан. Также мы используем @override, чтобы указать, что данное API определенное в родительском классе реализовано в дочернем классе. Аннотации всегда начинаются с символа @.
Как создать свою аннотацию?
Несмотря на то, что идея добавить метаданные к коду звучит немного экзотично и сложно, аннотации – это одна из самых простых вещей в языке Dart. Ранее было сказано, что аннотации просто несут дополнительную информацию. Они похожи на PODO (Plain Old Dart Objects). И любой класс может служить аннотацией, если в нем определен const конструктор:
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 {}Как вы можете заметить, аннотации очень просты. И основное значение имеет то, что мы будем делать с этими аннотациями. В этом нам помогут source_gen и build_runner.
Как использовать build_runner?
build_runner – это Dart пакет, который поможет нам сгенерировать файлы, используя Dart-код. Мы сконфигурируем Builder файлы, используя build.yaml. Когда он будет сконфигурирован, то Builder будет вызываться при каждой команде build или при изменении файла. У нас также есть возможность распарсить код, который был изменен или соответствует некоторым критериям.
source_gen для понимания Dart-кода
В некотором смысле, build_runner это механизм, который отвечает на вопрос «Когда нужно сгенерировать код?». Вместе с тем, source_gen отвечает на вопрос «Какой код должен быть сгенерирован?». source_gen предоставляет фреймворк, позволяющий создать Builders, для работы build_runner. Также source_gen предоставляет удобный API для парсинга и генерации кода.
Собираем все вместе: TODO-репорт
В оставшейся части статьи мы будем разбирать проект todo_reporter.dart, который может быть найден здесь.
Существует неписанное правило, которому следуют все проекты, использующие кодогенерацию: необходимо создать пакет, содержащий аннотации, и отдельный пакет для генератора, который использует эти аннотации. Информацию о том, как создать пакет-библиотеку в Dart/Flutter можно найти по ссылке.
Для начала нужно создать директорию todo_reporter.dart. Внутри этой директории нужно создать директорию todo_reporter, в которой будет находиться аннотация, директорию todo_reporter_generator для обработки аннотации и, наконец, директорию example, содержащую демонстрацию возможностей создаваемой библиотеки.
Суффикс .dart был добавлен к имени корневой директории для ясности. Конечно, это не обязательно, но мне нравится следовать этому правилу, чтобы точно обозначить тот факт, что данный пакет может быть использован в любом Dart-проекте. Напротив, если бы я хотел указать, что данный пакет – только для Flutter (как ozzie.flutter), я бы использовал другой суффикс. Делать это не обязательно, это просто соглашение об именовании, которого я стараюсь придерживаться.
Создание todo_reporter, нашего простого пакета с аннотацией
Мы собираемся создать todo_reporter внутри todo_reporter.dart. Для этого нужно создать файл pubspec.yaml и директорию lib.
pubspec.yaml очень прост:
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Тут нет зависимостей, кроме пакета test, используемого в процессе разработки.
В директории lib нужно сделать следующее:
- Нужно создать файл
todo_reporter.dart, в котором, используяexport, будут указаны все классы, имеющие публичный API. Это хорошая практика, так как любой класс в нашем пакете может быть импортирован при помощиimport 'package:todo_reporter/todo_reporter.dart';. Вы можете видеть этот класс здесь. - Внутри директории
libмы создадим директориюsrc, содержащую весь код – публичный и непубличный.
В нашем случае, все, что нам нужно добавить, это аннотация. Давайте создадим файл todo.dart с нашей аннотацией:
class Todo {
final String name;
final String todoUrl;
const Todo(this.name, {this.todoUrl}) : assert(name != null);
}Итак, это все, что нужно для аннотации. Я же говорил, что это будет просто. Но это еще не все. Давайте добавим unit-тесты в директорию test:
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);
});
});
}Это все, что нам нужно для создания аннотации. Код вы можете найти по ссылке. Теперь мы можем перейти в генератору.
Делаем классную работу: todo_reporter_generator
Теперь, когда мы знаем как создавать пакеты, давайте создадим пакет todo_reporter_generator. Внутри этого пакета должны быть файлы pubspec.yaml и build.yaml и директория lib. В директории lib должны быть директория src и файл builder.dart. Наш todo_reporter_generator считается отдельным пакетом, который будет добавлен как dev_dependency к другим проектам. Это сделано потому, что кодогенерация нужна только на этапе разработки, и ее не нужно добавлять в готовое приложение.
pubspec.yaml выглядит следующим образом:
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Теперь давайте создадим build.yaml. Этот файл содержит конфигурацию, необходимую для наших Builders. Более подробно можно почитать здесь. build.yaml выглядит следующим образом:
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"]Свойство import указывает на файл, котором содержится Builder, а свойство builder_factories указывает на методы, которые будут генерировать код.
Теперь мы можем создать файл builder.dart в директории 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');И файл todo_reporter_generator.dart в директории 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<Todo> {
@override
FutureOr<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
return "// Hey! Annotation found!";
}
}Как вы можете видеть, в файле builder.dart мы определили метод todoReporter, который создает Builder. Builder создается с помощью SharedPartBuilder, который использует наш TodoReporterGenerator. Так build_runner и source_gen работают вместе.
Наш TodoReporterGenerator является подклассом GeneratorForAnnotation, поэтому метод generateForAnnotatedElement будет выполняться только когда данная аннотация (@Todo в нашем случае) будет найдена в коде.
Метод generateForAnnotatedElement возвращает строку, содержащую наш сгенерированный код. Если сгенерированный код не скомпилируется, то вся фаза сборки потерпит неудачу. Это очень полезно, так как позволяет избежать ошибок в будущем.
Таким образом, при каждой генерации кода наш todo_repoter_generator будет создавать part файл, с комментарием // Hey! Annotation found! В следующей статье мы узнаем, как обрабатывать аннотации.
Собираем все вместе: использование todo_reporter
Теперь можно продемонстрировать работу todo_reporter.dart. Это хорошая практика – добавить example-проект при работе с пакетами. Так другие разработчики смогут увидеть как API может быть использовано в реальном проекте.
Давайте создадим проект и добавим требуемые зависимости в pubspec.yaml. В нашем случае, мы создадим Flutter проект внутри директории example и добавим зависимости:
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/После получения пакетов (flutter packages get) мы можем использовать нашу аннотацию:
import 'package:todo_reporter/todo_reporter.dart';
@Todo('Complete implementation of TestClass')
class TestClass {}Теперь, когда все на своих местах, запустим наш генератор:
$ flutter packages pub run build_runner buildПосле завершения работы команды вы заметите новый файл в нашем проекте: todo.g.dart. Он будет содержать следующее:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'todo.dart';
// *****************************************************************
// TodoReporterGenerator
// ********************************************************************
// Hey! Annotation found!Мы добились чего хотели! Теперь мы можем генерировать корректный Dart-файл для каждой аннотации @Todo в нашем коде. Пробуйте и создавайте их сколько потребуется.
В следующей статье
Теперь у нас есть корректные настройки для генерации файлов. В следующей статье мы узнаем как использовать аннотации, чтобы сгенерированный код мог делать по-настоящему классные вещи. Ведь тот код, который генерируется сейчас не имеет особого смысла.