В первой части мы выяснили зачем нужна кодогенерация и перечислили необходимые инструменты для кодогенерации в 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 в нашем коде. Пробуйте и создавайте их сколько потребуется.
В следующей статье
Теперь у нас есть корректные настройки для генерации файлов. В следующей статье мы узнаем как использовать аннотации, чтобы сгенерированный код мог делать по-настоящему классные вещи. Ведь тот код, который генерируется сейчас не имеет особого смысла.
