こんにちは、株式会社Pentagonでアプリ開発をしている石渡港です。
今回はソースコード自動生成手法の選定基準についてまとめました。
その結果、ソースコード自動生成手法が複数あることがわかりました。
【この記事を読むメリット】
- Flutterのソースコード自動生成手法について理解できる
【こんな方に参考にしていただきたい】
- Flutterのソースコード自動生成手法について悩んでいる人
【調査の動機】
弊社でFlutterのソースコード自動生成手法について調査した際、どのように決めるべきか悩んだため。
【調査結果】
- 調査した結果、Flutterのソースコード自動生成手法について理解できました。
- 弊社では、テンプレートを元にして、外部の設定ファイルからソースコードを生成する必要があり、buildを利用した手法が最適だとわかりました。
build_runnerを利用してソースコードを自動生成する方法
今回は、Flutterのライブラリであるbuild_runnerを用いて、アノテーションや文字列・設定情報を素にソースコードを生成する方法について調査してまとめました。
build_runnerとは
Dartコードを使用してファイルを生成するためのライブラリです。
Builder
ファイルで構成されているPackageとbuild.yaml
を参照して、ビルドスプリクトが走るようになっています。
ソースコード生成手法
名称 | 役割 | 何向きか |
---|---|---|
build | ソースコード生成のためのビジネスロジック | シンプルなソースコード生成向け |
source_gen | ソースコード生成のためのライブラリ | analyzerやbuildを利用した低レイヤー向け |
code_builder | クラスとメソッドのみソースコード生成するためのライブラリ | 簡易なクラスを作成したいとき向け |
次に、表に示したライブラリについて更に詳しく説明していきます。
build とは
build_runnerを用いて、ソースコードを生成するためのシンプルなライブラリです。
buildを用いてできること
文字列を元にソースコードを生成できます。
buildで再現できないこと
analyze(ソースコードの解析を行うライブラリ)を利用したアノテーションをもとに行うソースコードの生成はできません。
アノテーションをもとにしたソースコードの生成を行う場合は、 source_gen を利用する必要があります。
buildの利用方法
pubspec.yaml
…
build: ^2.3.0
…
build.yaml
targets:
$default:
builders:
プロジェクト名:
enabled: true
builders:
プロジェクト名:
import: "package:プロジェクト名/実行ファイル名.dart"
builder_factories: [ "build" ]
build_extensions: { "$lib$": [ ".gen.dart" ] }
auto_apply: dependents
build_to: source
実行ファイル名.dart
library プロジェクト名;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:build/build.dart';
Builder build(BuilderOptions options) {
Future(() async {
final file = File('lib/gen/生成ファイル名.gen.dart');
writeAsString(‘’’
class HogeClass {
}
’’’, file: file);
});
return EmptyBuilder();
}
class EmptyBuilder extends Builder {
@override
Future<void> build(BuildStep buildStep) async {}
@override
Map<String, List<String>> get buildExtensions => {};
}
void writeAsString(String text, {required File file}) {
if (!file.existsSync()) {
file.createSync(recursive: true);
}
file.writeAsStringSync(text);
}
source_gen とは
build_runnerを用いてコードを解析・生成するための使いやすいAPIを公開しているライブラリです。
アノテーション付加クラスの構成要素の提供を行うジェネレータクラスの提供や、目的別コードファイル生成を行うビルダークラスを提供しています。
source_genを用いてできること
analyzeやbuildを利用した低レイヤーのソースコードを生成できます。
source_genで再現できないこと
こちらのライブラリでほとんどのソースコード生成を対応できます。
source_genの利用方法
pubspec.yaml
…
source_gen: ^1.2.2
…
ソースコード
annotations.dart
class Multiplier {
final num value;
const Multiplier(this.value);
}
builder.dart
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'src/member_count_library_generator.dart';
import 'src/multiplier_generator.dart';
import 'src/property_product_generator.dart';
import 'src/property_sum_generator.dart';
Builder metadataLibraryBuilder(BuilderOptions options) => LibraryBuilder(
MemberCountLibraryGenerator(),
generatedExtension: '.info.dart',
);
Builder productBuilder(BuilderOptions options) =>
SharedPartBuilder([PropertyProductGenerator()], 'product');
Builder sumBuilder(BuilderOptions options) =>
SharedPartBuilder([PropertySumGenerator()], 'sum');
Builder multiplyBuilder(BuilderOptions options) =>
SharedPartBuilder([MultiplierGenerator()], 'multiply');
multiplier_generator.dart
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import '../annotations.dart';
class MultiplierGenerator extends GeneratorForAnnotation<Multiplier> {
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
final numValue = annotation.read('value').literalValue as num;
return 'num ${element.name}Multiplied() => ${element.name} * $numValue;';
}
}
利用側ソースコード
library_source.dart
import 'dart:math';
import 'package:source_gen_example/annotations.dart';
part 'library_source.g.dart';
@Multiplier(2)
const answer = 42;
const tau = pi * 2;
library_source.dart
とbuilder.dart
を元にlibrary_source.g.dart
が生成されます。
library_source.g.dart
part of 'library_source.dart';
// **************************************************************************
// MultiplierGenerator
// **************************************************************************
num answerMultiplied() => answer * 2;
// **************************************************************************
// PropertyProductGenerator
// **************************************************************************
num allProduct() => answer * tau;
// **************************************************************************
// PropertySumGenerator
// **************************************************************************
num allSum() => answer + tau;
library_source.info.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// MemberCountLibraryGenerator
// **************************************************************************
// Source library: package:source_gen_example_usage/library_source.dart
const topLevelNumVarCount = 2;
code_builderの概要
build_runnerを利用する点はsource_genと同様で、インポート、クラス名、継承元、メソッド名、メソッド内容と限られた狭い範囲を編集できるAPIを提供しています。
code_builderを用いてできること
簡単なソースコードを生成できます。
code_builderで再現できないこと
複雑なメソッドを作成できません。もし、複雑なメソッドを利用したソースコードを生成したい場合、シンプルなコード生成である場合は build をおすすめします。複雑なカスタムやアノテーションを利用したい場合は source_genで対応可能です。
code_builderの利用方法
pubspec.yaml
…
code_builder: ^4.1.0
…
実行ファイル名.dart
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
void main() {
final class = Class((b) => b
..name = 'クラス名'
..extend = refer('継承元クラス')
..methods.add(Method.returnsVoid((b) => b
..name = 'メソッド名'
..body = const Code("メソッド内容("print('Yum');")"))));
final emitter = DartEmitter();
print(DartFormatter().format('${class.accept(emitter)}'));
}
まとめ
buildはシンプルなソースコード生成に向いています。
code_builderは簡単なメソッドのみを扱うクラスをソースコード生成する際に最適です。
source_genはフルカスタムのクラスを作成したいときに使えるとわかりました。
また、アノテーションを利用したクラスを作成する際に使ってみると良いと思います。