[Flutter] コード自動生成ツールmasonを使い倒してみる [実例]
概要
Flutterアプリ開発において、画面のボイラープレートを作成するのは面倒な作業ですが、masonを使えば、ViewModel、State、そして画面のテンプレートを一瞬で生成することができます。この記事では、masonのインストールから、カスタムテンプレートの作成、そしてhookを使った柔軟なコード生成まで、実践的な使い方を段階的に紹介します。
masonを導入することで、毎回同じようなコードを書く手間が省け、開発スピードが飛躍的に向上します。ぜひ、masonを活用してFlutterアプリ開発を効率化しましょう!
要件
- 画面のボイラープレートと、それに付随するViewModelとStateを生成する。
- 生成した瞬間からビルドできる状態になっている。
masonのインストール
残念ながら、 asdf にはmasonのプラグインが見当たりませんでした。そのため、今回は Homebrew を使ってインストールします。
brew tap felangel/mason
brew install mason
masonは内部でdartコマンドを使用しているため、dartのバージョンが古い場合は、先に最新版にアップデートしておくことをおすすめします。
mason init
プロジェクトのルートディレクトリで以下のコマンドを実行すると、 mason.yaml
ファイルが生成されます。
mason init
mason new
次に、テンプレートを作成していきます。今回は ${projectRoot}/mason
ディレクトリ以下にテンプレートを配置することにします。
例として、view_model というテンプレートを作成してみましょう。 mason new
コマンドを実行すると、${projectRoot}/mason/view_model
(以下、テンプレートディレクトリと呼称) 配下にテンプレート一式が生成されます。
--hooks
オプションを使用すると、自動でhooks(後述)を追加できますが、後から追加することも可能です。なお、グローバル(自分のローカル環境)にテンプレートを作成して使用することもできるようです。
$ mkdir mason
$ cd mason
$ mason new view_model
$ cd view_model
$ tree
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __brick__
│ └── HELLO.md
└── brick.yaml
登録する
mason make
コマンドを使ってテンプレートから実際のコードを生成するには、事前に mason add
コマンドでテンプレートを登録する必要があります。
mason add view_model --path ./mason/view_model
生成する
以下のコマンドを実行することで、テンプレートからコードを生成できます。
mason make view_model --output-dir ./lib/{出力先}
テンプレートをカスタマイズする
基本的な説明は省略しますが、変数の文字列中の /
がエスケープされてしまう問題があります。これを回避するには、 {{{}}}
で変数を囲むと良いでしょう。
__brick__
ディレクトリ以下の内容が、そのまま --output-dir
で指定したディレクトリに展開されるイメージです。
import 'package:.../service/logger/logger_provider.dart';
{{{viewStateImport}}}
import 'package:riverpod_annotation/riverpod_annotation.dart';
part '{{name}}_view_model.g.dart';
@Riverpod(keepAlive: false, dependencies: [logger])
class {{name.pascalCase()}}ViewModel extends _${{name.pascalCase()}}ViewModel {
@override
Future<{{name.pascalCase()}}ViewState> build() async {
return {{name.pascalCase()}}ViewState.initial();
}
}
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __brick__
│ ├── view_model
│ │ ├── {{name}}_view_model.dart
│ │ └── {{name}}_view_state.dart
│ └── {{name}}_screen.dart
├── brick.yaml
└── hooks (...)
テンプレートの入力項目の設定
brick.yaml
を編集することで、テンプレートの入力項目をカスタマイズできます。詳細は割愛しますが、今回は .vars.name
を編集しました。
name: view_model
description: A new brick created with the Mason CLI.
version: 0.1.0+1
environment:
mason: ">=0.1.0-dev.53 <0.1.0"
vars:
name:
type: string
description: ViewModel name
default: Dash
prompt: What is ViewModel name?
hook
pre_gen.dart
の hook を使うことで、コード生成前に任意の処理を実行させることができます。 post_gen
の hook も用意されているようです。
hook
を有効にするには、テンプレートディレクトリ以下に hooks
ディレクトリ(dartプロジェクト)を作成し、その中に pre_gen.dart
を配置します。 pubspec.yaml
と pre_gen.dart
を用意したら、 dart pub get
を実行しておきましょう。
name: hooks
environment:
sdk: "^3.0.0"
dependencies:
mason: ^0.1.0-dev
path: ^1.9.0
import 'dart:io';
import 'package:mason/mason.dart';
void run(HookContext context) {
final name = context.vars['name'] as String;
}
.
(...)
├── __brick__ (...)
└── hooks
├── build (...)
├── pre_gen.dart
├── pubspec.lock
└── pubspec.yaml
pre_genで入力を加工する
今回のケースでは、生成したファイル間で相互にimportが必要になるため、それを自前で生成してあげないと、生成直後にビルドエラーになってしまいます。そこで、 viewStateImport
という変数にimport文を詰めて対応しています。8割方、関数名だけ書いてCopilot君に生成してもらったコードなので、マサカリはCopilot君に投げましょう。
ポイントとしては、 masonのパッケージがString
の拡張関数群をexportしているので、 .snakeCase
で文字列をスネークケース化できたりします。あとは、logger.err
を呼んでもエラーにならないという難点を抱えています(詳細、意図などは未調査です)。
import 'dart:io';
import 'package:mason/mason.dart';
import "package:path/path.dart" show dirname, join, relative;
void run(HookContext context) {
final name = context.vars['name'] as String;
context.vars['name'] = name.snakeCase;
final packagePath =
getPackagePathOf("view_model/${name.snakeCase}_view_state.dart");
final viewStateImport =
"import 'package:${getPackageName()}/${packagePath}';";
context.vars['viewStateImport'] = viewStateImport;
}
String getPackagePathOf(String relativePath) {
final packageRoot = relative(Directory.current.path, from: getProjectRoot())
.replaceAll('lib/', '');
return join(packageRoot, relativePath);
}
String getProjectRoot() {
return getProjectRootRecursively(Directory.current.path);
}
String getProjectRootRecursively(String current) {
if (isProjectRoot(current)) {
return current;
}
return getProjectRootRecursively(dirname(current));
}
bool isProjectRoot(String path) {
final pubspec = File(join(path, 'pubspec.yaml'));
if (pubspec.existsSync()) {
return true;
}
return false;
}
String getPackageName() {
final pubspec = File(join(getProjectRoot(), 'pubspec.yaml'));
final content = pubspec.readAsStringSync();
final packageName = RegExp(r'name: (.+)').firstMatch(content)?.group(1);
if (packageName == null) {
throw Exception('Could not find package name in pubspec.yaml');
}
return packageName;
}
懺悔
- mason以外に良いツールがあるのかは調べていないです。
- 概要だけClaude君に書いてもらいました。
- GitHubのイシューやソースコードは一部確認しましたが、READMEは読み込んでいないので、このユースケースが適切かどうかは自信がありません。
Discussion