自動コード生成を駆使してFlutter開発を楽にする

# はじめに

この記事は[Flutter Advent Calendar 2019](https://qiita.com/advent-calendar/2019/flutter) 6日目の記事です。

前日は @mkosuke さんの[Effective Dartまとめ](https://qiita.com/mkosuke/items/42c19d7edbf111f7fb71)でした。
Effective DartにはDartらしいコードを書くためのエッセンスが詰まっていますので、これが日本語で読めるのはとても素晴らしいことだと思います。

この記事ではFlutter開発でも使えるDartの自動コード生成のお話をします。

# 自動コード生成とは

プログラムにソースコードを生成させる仕組みがあります。
Android開発をしている方なら、Javaでは[JSR-269](https://jcp.org/en/jsr/detail?id=269)または[apt](https://docs.oracle.com/javase/jp/7/technotes/guides/apt/index.html)、Kotlinでは[kapt](https://kotlinlang.org/docs/reference/kapt.html)が広く知られていますし、
iOS開発されている方の中でも[Sourcery](https://github.com/krzysztofzablocki/Sourcery)や[SwiftGen](https://github.com/SwiftGen/SwiftGen)を使ったことがある方も多いかと思います。

また、[gRPC](https://grpc.io/)や[Swagger(OpenAPI)](https://swagger.io/)で生成されたコードを使用する機会もあるかと思います。
こうしたものも自動コード生成を活用していると言えるでしょう。


## どうして必要なのか

ボイラープレートコードを人間が書かなくて済むようにするためです。

ボイラープレートコードとは、ある処理をするために書く必要がある決まりきったコードを指します。


例えば、あるenumから、その定数を表現する文字列を取り出す必要があるとします。

```dart
// 都道府県を表現する
enum Prefecture {
  hokkaido,
  aomori,
  ...
}

extension PrefectureLabel on Prefecture {
  // 都道府県名を取得する
  String get label {
    switch (this) {
      case Prefecture.hokkaido:
        return "北海道";
      case Prefecture.aomori:
        return "青森";
      ... // 長くてやってらんない 😡
    }
  }
}

// 世界の国と地域
enum CountriesAndRegions {
  iceland,
  ireland,
  ...
}

extension CountriesAndRegionsLabel on CountriesAndRegions {
  String get label {
    switch (this) {
      case CountriesAndRegions.iceland:
        return "アイスランド";
      ... // まだあるの！？！？ 💢
    }
  }
}
```
上記の場合、extensionとその中身がボイラープレートコードになります。
enum定数に対して、返却する値以外は書くべきコードが同じであるためです。

enumの定義だけならまだしも、そのextensionのswitch-caseまでいちいち書きたくありませんね！
頑張って全部手書きしますか？　したくないですね！
せめてenum定義と一緒に表現を持たせられれば、まだ楽になるのですが。

```dart
// Swiftみたいにこう書けたらいいな（Dart 2.6現在、enumに値をもたせることは出来ません）
enum Prefecture {
  hokkaido = "北海道",
  aomori = "青森",
  ...
}
```

Dartのenumは値や実装を持てない制約があるため、こればかりはどうしようもありません。
都道府県は47個で収まりますが（それでも十分きついのですが）、世界の国と地域のenumだったら何度もswitch-caseを書くのは地獄を見そうですね…

## コード生成以外の手段

リフレクションでも同様の問題を解決することができます。

```dart
// 情報を付与するアノテーション
class Value {
  final String value;
  const Value(this.value);
}

// アノテーション（メタデータ）で情報を持たせてみる
enum Prefecture {
  @Value("北海道")
  hokkaido,
  @Value("青森")
  aomori,
  ...
}

extension PrefectureLabel on Prefecture {
  // 都道府県名を取得する
  String get label {
    final instanceMirror = reflect(this);
    final classMirror = instanceMirror.type;
    final thisSymbol = Symbol(toString().split(".")[1]);
    return classMirror
      .declarations[thisSymbol]
      .metadata
      .first
      .getField(#value)
      .reflectee
  }
}
```

`dart:mirrors` を使用できる環境であれば、上記で各メタデータで指定された値を取り出すことが出来ます。

[しかしFlutterでは`dart:mirrors`を使用することができません。](https://github.com/flutter/flutter/issues/1150)
どうやら最適化のためのようです。

AndroidでもP8（Proguard）による最適化や難読化の問題、コンパイル時に方の不整合などを検出出来ない問題などがあり、リフレクションを使用したライブラリは減ってきているように思います。

それに上記の方法では、「各enum全てに対してextensionを書かなければいけない」という問題も解決できていません。
コード生成はこれらの問題も解決できる場合があります。


## 利用例

[built_value](https://pub.dev/packages/built_value)というツールがあります。
built_valueはいわゆるimmutableな値オブジェクトの生成を簡単にしてくれます。

```dart
part "user.g.dart"

abstract class User implements Built<User, UserBuilder> {
  static Serializer<User> get serializer => _$userSerializer;
  String get name;

  User._();
  factory User([void Function(UserBuilder) updates]) = _$User;
}
```

こうしたコードを書いておくと、

* メンバの内容を `==` で比較する `operator ==`
* `operator ==` を オーバーライドするなら、こちらもオーバーライドする必要がある `hashCode`
* 内容がわかる `toString`
* ビルダーバターンで必要なメンバだけを更新して新しい値を作る `rebuild`
* ビルダーバターンで新しい値を作るコンストラクタ
* 上記ビルダーバターンを実現するための `UserBuilder` クラスとその実装
* `[(メンバ名), (メンバの値), ...]` という形でリストにシリアライズしてくれるシリアライザと、そのデシリアライザ

を生成してくれます。
自分で書いた分量に対して結構な量のコードを吐いてくれるので、それだけ実装を楽できたことを実感できます。


# 実演：アノテーションつきenumの拡張関数を自動生成する

[どうして必要なのか](#どうして必要なのか)に書いた、enumに対して特定のStringが欲しい場合もswitch-caseを含んだextensionを書かないといけない問題を解決してみましょう。

## 理想形を決める

まずはどんな形にできたら理想的なのか、ビフォー・アフターを考えます。

今回はアノテーションを用いてenum定数に値を保持させ、アノテーションがついているメンバを持つenumについてはextensionを生成する、という形にしたいと思います。

## プロジェクトを分ける

自動生成を行う前に考える必要があるのが、プロジェクトの分割です。
生成には以下の要素が必要になります。

* アプリケーションに含めるライブラリ
    * アノテーションクラス
    * 生成したコードから使用するユーティリティなど
* コードの解析と生成を行うジェネレーター
    * この後詳しく解説する部分

アプリケーションにジェネレーターのコードを含めてしまうと、アプリケーションからは使用しないライブラリなどが含まれることになってしまい、特に良いことがありません。

一つのリポジトリの中に、2つのpubパッケージを作るのが良いと思います。

## アノテーションクラスを作る

Dartでは、デフォルトコンストラクタが`const`コンストラクタになっている（そのためには、すべてのメンバが`final`になっている必要がある）クラスがアノテーションとして使用できます。

```dart
class Value {
  final String value;
  const Value(this.value): assert(value != null);
}
```
これはライブラリのパッケージに含めておきます。

## ジェネレーターを作る

以下の説明はジェネレーターのパッケージについてのものです。

### 関連ライブラリ

|名称|役割|
|:--:|:--:|
|（上記で作成した、アプリケーションに含めるライブラリ）|アノテーションクラスを参照するために必要です|
|[`build`](https://pub.dev/packages/build)|`build_runner`が実行するステップを記述するためのものです|
|[`analyzer`](https://pub.dev/packages/analyzer)|既存のDartソースコードを解析して、構文要素をプログラムから操作できるようにしてくれます|
|[`source_gen`](https://pub.dev/packages/source_gen)| `build`と`analyzer`を組み合わせて、ソースコードの生成手段を提供します|
|[`code_builder`](https://pub.dev/packages/code_builder)|型安全にDartのソースコードを構成します。KotlinPoetみたいなもの|
|[`dart_style`](https://pub.dev/packages/dart_style)|生成したDartのソースコードを綺麗に整形してくれるもの|
|[`build_runner`](https://pub.dev/packages/build_runner) |（これは、自動生成を行いたいパッケージに取り込ませます。）パッケージに対して、定義したビルド操作を行います。Makeみたいなもの|

~~Dartのこの辺のツールみんなGooglabilityが低くて困る~~

必要に応じてプロジェクトに依存関係を追加してください。


### ジェネレータクラスを作る

[`source_gen`](https://pub.dev/packages/source_gen)を使います。

とりあえずプロジェクトに取り込んで、生成処理を行うクラスを作ってみましょう。
`Generator` クラスを継承して `generate()` を実装します。


```dart
class EnumHasValueGenerator extends Generator {

  final TypeChecker hasValue = TypeChecker.fromRuntime(Value);

  @override
  FutureOr<String> generate(LibraryReader library, BuildStep buildStep) {
    final lib = Library((b) => b
      ..body.addAll(library.enums.map((e) => Code(_codeForEnum(e))))
    );
    final emitter = DartEmitter();
    return lib.accept(emitter).toString();
  }

  String _codeForEnum(ClassElement enum$) {
    return """
    extension ${enum$.name}ValueExtension on ${enum$.name} {
      String get value {
        switch (this) {
          ${enum$.annotatedWith(hasValue).map((f) => """
          case ${enum$.name}.${f.element.name}:
            return \"${f.annotation.read("value").stringValue}\";
          """).join()}
        default:
          return this.toString();
        }
      }
    }
    """;
  }
}

// ヘルパーメソッド類は省略
```

`generate()` でreturnしたソースコードを、自動的にファイルに落としてくれます。

ソースコードの記述には [`code_builder`](https://pub.dev/packages/code_builder)を使用します。
これを使うと型の恩恵を受けつつDartのソースコードを組むことが出来ます[^code_builder_extension]。
[^code_builder_extension]: code_builder 3.2.0時点ではまだextension記法に対応していないため、extension部分を手書きで書いています。



### ジェネレーターの設定をする

```dart
Builder enumHasValueBuilder(BuilderOptions options) =>
  // ファイル名の接尾辞が .value.g.dart になるようにしました
  PartBuilder([EnumHasValueGenerator()], ".value.g.dart");
```

こうした関数を宣言しておいた後、パッケージトップに `build.yaml` というファイルを用意しておきます。
これで、このパッケージが `build_runner` から使用できるようになります。

```yaml
builders:
  enum_string_value:
    import: "package:enum_string_value_generator/enum_string_value_generator.dart"
    builder_factories: ["enumHasValueBuilder"]
    build_extensions: {".dart": [".value.g.dart"]}
    auto_apply: dependents
    build_to: source
    applies_builders: ["source_gen|combining_builder"]
```



## 導入して使う

作成したジェネレーターを使いたいパッケージにて、`build_runner`と先程作成した2つのプロジェクトを依存関係に追加し、トップに `build.yaml` を作成します。

```yaml
targets:
  $default:
    builders:
      enum_string_value_generator|enumHasValueBuilder:
        generate_for:
          - lib/*.dart
```

あとは以下のコマンドを叩くことで、ソースファイルが生成されます！！

```
$ pub run build_runner build
```

## できあがり

https://github.com/kikuchy/enum_string_value

こちらに全部のソースコードを載せています。
まだFlutterパッケージで使用できるか試していませんが、Dart 2.6.0以降が使用できるプロジェクトならば使用できるかもしれません。


# おわりに

自動コード生成でどんどん人間の仕事を減らして、楽して生きていきましょう！！！