Dart でテンプレートからファイル生成を行う

$ dart ./scripts/page_generator.dart sign_in

みたいにしたら sign_in_page.dart, sign_in_view_mode.dart, sign_in_view_state.dart みたいに必要なファイル群をガッっと生成してくれるやつを作る。iOS でいう Generamba とかそういう系のやつ。

既存のコードをロードして何かをするわけではなく、単純なファイル生成なので source_gen などは利用しない。

page_generator.dart

仕組みは単純で、引数の値を利用してテンプレートのStringにclass名を埋め込んだりファイル名を生成して適当なフォルダにファイルを生成するだけ。

import 'dart:io';

import 'package:dart_style/dart_style.dart';

void main(List<String> args) async {
  // 引数チェック
  if (args.length != 1) throw ArgumentError('Must have only one argument');

  // 例: sign_in
  final directoryName = args.first;

  // lib/pages/sign_in ディレクトリを作成
  final directory =
      await Directory('lib/pages/$directoryName').create(recursive: true);

  // sign_in を SignIn に変換
  final classPrefix = directoryName
      .split('_')
      .map((v) => v[0].toUpperCase() + v.substring(1))
      .join();

  // ここまでに作成した値を使いコード生成
  await Future.wait([
    _writePageCode(classPrefix, directory, directoryName),
    _writeViewModelCode(classPrefix, directory, directoryName),
    _writeViewStateCode(classPrefix, directory, directoryName),
  ]);
}

Future<void> _writePageCode(
    String classPrefix, Directory directory, String directoryName) async {
  // sign_in_page.dart がファイル名となる
  final fileName = directoryName + '_page.dart';
  final code = '''
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class ${classPrefix}Page extends HookConsumerWidget {
  const ${classPrefix}Page({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return const Scaffold();
  }
}
''';

  // lib/pages/sign_in/sign_in_page.dart が生成される
  final file = File('${directory.path}/$fileName');
  // フォーマットしてファイル作成
  await file.writeAsString(DartFormatter().format(code));

  stdout.writeln('${file.path} generated.');
}

Future<void> _writeViewModelCode(
    String classPrefix, Directory directory, String directoryName) async {
  final fileName = directoryName + '_view_model.dart';
  final providerNamePrefix =
      classPrefix[0].toLowerCase() + classPrefix.substring(1);
  final code = '''
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '${directoryName}_view_state.dart';

final ${providerNamePrefix}ViewModelProvider =
    StateNotifierProvider.autoDispose<${classPrefix}ViewModel, ${classPrefix}ViewState>(
  (ref) {
    return ${classPrefix}ViewModel();
  },
);

class ${classPrefix}ViewModel extends StateNotifier<${classPrefix}ViewState> {
  ${classPrefix}ViewModel() : super(const ${classPrefix}ViewState());
}
''';

  final file = File('${directory.path}/$fileName');
  await file.writeAsString(DartFormatter().format(code));

  stdout.writeln('${file.path} generated.');
}

Future<void> _writeViewStateCode(
    String classPrefix, Directory directory, String directoryName) async {
  final fileName = directoryName + '_view_state.dart';
  final code = '''
import 'package:freezed_annotation/freezed_annotation.dart';

part '${directoryName}_view_state.freezed.dart';

@freezed
class ${classPrefix}ViewState with _\$${classPrefix}ViewState {
  const factory ${classPrefix}ViewState({
    @Default(false) loading,
  }) = _${classPrefix}ViewState;
}
''';

  final file = File('${directory.path}/$fileName');
  await file.writeAsString(DartFormatter().format(code));

  stdout.writeln('${file.path} generated.');
}

生成されたコード

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class SignInPage extends HookConsumerWidget {
  const SignInPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return const Scaffold();
  }
}
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'sign_in_view_state.dart';

final signInViewModelProvider =
    StateNotifierProvider.autoDispose<SignInViewModel, SignInViewState>(
  (ref) {
    return SignInViewModel();
  },
);

class SignInViewModel extends StateNotifier<SignInViewState> {
  SignInViewModel() : super(const SignInViewState());
}
import 'package:freezed_annotation/freezed_annotation.dart';

part 'sign_in_view_state.freezed.dart';

@freezed
class SignInViewState with _$SignInViewState {
  const factory SignInViewState({
    @Default(false) loading,
  }) = _SignInViewState;
}

おわり

最初はコード生成などで 自動コード生成を駆使してFlutter開発を楽にする - Qiita の記事などを読んでいたが、今回は既存のコードをロードしてそこからコードを生成せず、単純なファイル生成だけなので source_gen | Dart Package などは利用する必要がなくシンプルにStringに対し文字列を埋め込むだけで実装できた。