【Flutter】ソースコード自動生成手法の選定基準について

こんにちは、株式会社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を用いてできること

analyzebuildを利用した低レイヤーのソースコードを生成できます。

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.dartbuilder.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はフルカスタムのクラスを作成したいときに使えるとわかりました。
また、アノテーションを利用したクラスを作成する際に使ってみると良いと思います。

採用情報はこちら
目次