😽

BuildRunnerでDartのコードを自動生成してみる

2021/04/09に公開

前回はcode_builderを使ってJSONからDartのコードを作成してみた。

https://zenn.dev/umatoma/articles/ef2f518a06df76

今回はbuild_runnerを使って、コード生成の処理を自動化(コマンド化)してみたいと思う。

build_runner・build_config・build

build_runnerを使うにあたり、必要となるパッケージがいくつかある。ちょっと分かりづらいので整理してみる。

build_runner

いわゆるタスクランナーみたいなもの。こいつが、build_config・buildを使って自動生成の処理を行ってくれる。

build_config

build.yamlを読み取ってくれるやつ。build.yamlの書き方を把握する以外は今の所使っていない。

build

自動生成の処理を記述する本体(Builder)。build_runnerから処理対象のファイルが入力されるので、処理したい内容をここに記述する。

Builderを作ってみる

全体のイメージはこんな感じ。

  • Builderを定義する
  • Builderを生成するためのFactoryを定義する
  • build.yamlに使用するBuilderと入力するファイルを定義する
  • build_runnerを通して処理を実行する

自動生成の処理自体は前回と同じなのでコピペ。自作のBuilderを定義するにはBuilderを継承したクラスを定義すればOK。Builder#build()に処理対象のファイルが渡ってくるので、後は好きに処理を書けば良い。

また、build.yamlにbuilder_factoriesというパラメータを指定する必要があるので、Builder factoryName(BuilderOptions options) な関数を定義する。

import 'dart:async';
import 'dart:convert';

import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';

Builder myBuilderFactory(BuilderOptions options) {
 return MyBuilder();
}

class MyBuilder extends Builder {
 
 Map<String, List<String>> get buildExtensions => {
       '.json': ['.json.dart'],
     };

 
 FutureOr<void> build(BuildStep buildStep) async {
   final inputId = buildStep.inputId;
   final outputId = inputId.addExtension('.dart');
   final json = await buildStep.readAsString(inputId);
   final contents = jsonToDart(json);
   await buildStep.writeAsString(outputId, contents);
 }

 String jsonToDart(String json) {
   final outputs = <String>[];
   final emitter = DartEmitter();
   final formatter = DartFormatter();

   final data = jsonDecode(json) as Map<String, dynamic>;
   for (final entry in data.entries) {
     final fields = entry.value as Map<String, dynamic>;
     final klass = Class((b) {
       // クラス名
       b.name = entry.key;

       // コンストラクタ
       b.constructors.add(Constructor(
         // Namedパラメータ
         (b) => b.optionalParameters.addAll([
           for (final entry in fields.entries)
             Parameter((b) => b
               ..name = entry.key
               ..named = true
               ..toThis = true)
         ]),
       ));

       // インスタンス変数
       b.fields.addAll([
         for (final entry in fields.entries)
           Field((b) => b
             ..name = entry.key
             ..modifier = FieldModifier.final$
             ..type = Reference(entry.value)),
       ]);
     });

     // クラスをコードとして出力
     outputs.add(formatter.format('${klass.accept(emitter)}'));
   }

   return outputs.join('\n');
 }
}

build.yamlには使用するBuilderの情報を記述していく。Builderが含まれるパッケージを外部パッケージとして切り出している場合はdependentsを、切り出してなくてソースコードの一部になってる時はroot_package辺りを指定しておけば良さそうな感じ。

また、targetsに関しては必須で指定する必要はないが、特定のディレクトリのファイルのみ処理対象としたい場合などは指定すると良いと思う。

builders:
 my_builder:
   import: 'package:my_builder/my_builder.dart'
   builder_factories: ['myBuilderFactory']
   build_extensions: {'.json': ['.json.dart']}
   auto_apply: root_package
   build_to: source
targets:
 $default:
   builders:
     my_builder:
       generate_for:
         - lib/*.json

あとは、コマンドを実行すればOK。lib/my.jsonなどを設置しておけば、my.json.dartが自動生成されるようになる。

$ dart run build_runner build
[INFO] Generating build script completed, took 337ms
[INFO] Reading cached asset graph completed, took 44ms
[INFO] Checking for updates since last build completed, took 419ms
[INFO] Running build completed, took 165ms
[INFO] Caching finalized dependency graph completed, took 20ms
[INFO] Succeeded after 195ms with 1 outputs (1 actions)

{
   "User": {
       "id": "String",
       "name": "String",
       "age": "int",
       "createdAt": "DateTime",
       "updatedAt": "DateTime"
   },
   "Post": {
       "id": "String",
       "title": "String",
       "body": "String",
       "createdAt": "DateTime",
       "updatedAt": "DateTime"
   }
}
class User {
 User({this.id, this.name, this.age, this.createdAt, this.updatedAt});

 final String id;

 final String name;

 final int age;

 final DateTime createdAt;

 final DateTime updatedAt;
}

class Post {
 Post({this.id, this.title, this.body, this.createdAt, this.updatedAt});

 final String id;

 final String title;

 final String body;

 final DateTime createdAt;

 final DateTime updatedAt;
}

最後に

設定が必要な項目が少なく、Builderとbuild.yamlさえ用意してしまえば簡単に作れてしまうのは良いところ。

次回はアノテーションを使って、DartからDartを作るようなBuilderを作ってみたいと思う。

作って学ぶ、FlutterとFirebaseを使ったアプリ開発

FlutterとFirebaseを使ったアプリ開発に関して書籍にまとめました。

Flutterで始めるアプリ開発

https://www.flutter-study.dev/

Discussion