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