В первой части мы выяснили зачем нужна кодогенерация и перечислили необходимые инструменты для кодогенерации в 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:


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);
    });
  });
}

Это все, что нам нужно для создания аннотации. Код вы можете найти по ссылке. Теперь мы можем перейти в генератору.


Делаем классную работу: 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 в нашем коде. Пробуйте и создавайте их сколько потребуется.


В следующей статье


Теперь у нас есть корректные настройки для генерации файлов. В следующей статье мы узнаем как использовать аннотации, чтобы сгенерированный код мог делать по-настоящему классные вещи. Ведь тот код, который генерируется сейчас не имеет особого смысла.

Комментарии (0)