👨‍💻

【Dart】Dart Macrosを使ってみる

2024/04/05に公開

初めに

今回は Dart の Macros を使ってみたいと思います。
なお、2024年4月5日現在 Dart の Macros は Dart では実行できるものの、 Flutter では使用することができないためご注意ください。

Macros についての詳しい説明の前にXの投稿を2つ引用させて頂きます。
まずは以下の投稿をご覧ください。
https://x.com/SandroMaglione/status/1752682717563568419?s=20

この投稿では、Macros を使うとデータクラスを定義するコードを大幅に短縮できるとされています。

また、以下の投稿もご覧ください。
https://x.com/spydon/status/1752629743222993184

この投稿では、コード生成や freezed、json_serializable を使用せずにデータクラス定義が可能であるとされています。つまり、コード生成を行う build_runner の flutter pub run build_runner build --delete-conflicting-outputs コマンドも実行不要になります。

個人的には Macros を使用するメリットとして以下の二つが大きいかと思いました。

  • データクラス定義等に関して build_runner のコマンドを実行しないで済む
  • データクラスの記述が短縮できる

Dart の Macros とは

次に Macros の説明に移りたいと思います。
Dart の Macros とは、dart-lang / macros の記述によると、以下のように定義されています。

マクロとは、コンパイル時にプログラムの他の部分を変更できるコードの一部である

また、Macros は Dart における Static Metaprograming をモチベーションとして開発されています。
Static Metaprograming についてはDart の Static Metaprogramming の記述 を以下に引用します。

Metaprogramming refers to code that operates on other code as if it were data. It can take code in as parameters, reflect over it, inspect it, create it, modify it, and return it. Static metaprogramming means doing that work at compile-time, and typically modifying or adding to the program based on that work.

(日本語訳)
メタプログラミングとは、他のコードをあたかもデータであるかのように操作するコードを指す。メタプログラミングは、コードをパラメータとして受け取り、それを反映し、検査し、作成し、修正し、返すことができる。静的メタプログラミングとは、コンパイル時にその作業を行い、通常はその作業に基づいてプログラムを修正したり追加したりすることを意味する。

上記にある通り、コンパイル時にコードを検査、作成、修正することで、問題があればコンパイル時にエラーを出してくれるようになります。したがって、より素早くコードの修正を行うことができるようになります。

説明を合わせると、 Macros はコンパイル時にプログラムの一部を変更できるものであり、その変更の内容に問題があればコンパイルエラーとして出力されるため、早い段階で気づくことができ効率も上がるということがわかります。

記事の対象者

  • Flutter, Dart 学習者
  • Dartの新しい機能を試してみたい方

目的

今回は上記の Macros を Dart で使ってみることを目的とします。なお Dart に関して理解が浅い部分も多々あるため、誤っている部分等あればご指摘いただければ幸いです。

最終的には Macros で定義されたモデルを用いて、 APIから取得した JSON 形式のデータをカスタムデータとして扱う実装を行いたいと思います。

準備

Macros を使うためには以下のステップが必要です。

  1. Flutter, Dart のバージョンアップ
  2. VSCode の拡張機能追加
  3. pubspec.yaml の編集
  4. analysis_options.yaml の編集

なお、自身の環境は以下の通りです。

  • Flutter version 3.22.0-5.0.pre.4 on channel master
  • Dart version 3.5.0 (build 3.5.0-18.0.dev)
  • VS Code (version 1.88.0)

1. Flutter, Dart のバージョンアップ

サンプルプロジェクトの README をみると、Flutter に関しては master channel に切り替える必要があります。また、Dart のバージョンもあげておく必要があります。
したがって、 Macros を実行したいプロジェクトに移動して以下を実行します。

flutter upgrade

2. VSCode の拡張機能の変更

次に VSCode の Dart, Flutter 拡張機能を変更します。
Dart の拡張機能の「Switch to Pre-Release Version」ボタンを選択して、リリース前の機能にもアクセスできるようにします。

Flutter の拡張機能も同様に「Switch to Pre-Release Version」ボタンを選択します。

ボタンを選択した後、プロジェクトに反映させるために一度VSCodeを再起動しておきましょう。

3. pubspec.yaml の編集

次にプロジェクトの pubspec.yamlサンプルプロジェクトのpubspeck.yamlを参考に変更します。

pubspec.yaml
dependencies:
  macros: any

dev_dependencies:
  _fe_analyzer_shared: any

dependency_overrides:
  macros:
    git:
      url: https://github.com/dart-lang/sdk.git
      path: pkg/macros
      ref: main
  _fe_analyzer_shared:
    git:
      url: https://github.com/dart-lang/sdk.git
      path: pkg/_fe_analyzer_shared
      ref: main

4. analysis_options.yaml の編集

次に analysis_options.yaml に関しても サンプルプロジェクトのanalysis_options.yaml に従って変更します。

analysis_options.yaml
analyzer:
  enable-experiment:
    - macros

これで準備は完了です。

実装

次に Macros で定義されたモデルを用いて、 APIから取得した JSON 形式のデータをカスタムデータとして扱う実装を行います。
実装は以下の手順で行います。

  1. モデルを Macros で定義
  2. 使用するデータモデルを作成
  3. APIからデータを取得して表示させる処理を実装
  4. 実行と結果

1. モデルを Macros で定義

まずは Macros でモデルを作成していきます。
コードは以下の通りです。

import 'dart:async';
import 'package:macros/macros.dart';

macro class Model implements ClassDeclarationsMacro {
  const Model();

  static const _baseTypes = ['bool', 'double', 'int', 'num', 'String'];
  static const _collectionTypes = ['List'];

  
  Future<void> buildDeclarationsForClass(
    ClassDeclaration classDeclaration,
    MemberDeclarationBuilder builder,
  ) async {
    final className = classDeclaration.identifier.name;

    final fields = await builder.fieldsOf(classDeclaration);

    final fieldNames = <String>[];
    final fieldTypes = <String, String>{};
    final fieldGenerics = <String, List<String>>{};

    for (final field in fields) {
      final fieldName = field.identifier.name;
      fieldNames.add(fieldName);

      final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;
      fieldTypes[fieldName] = fieldType;

      if (_collectionTypes.contains(fieldType)) {
        final generics = (field.type.code as NamedTypeAnnotationCode)
            .typeArguments
            .map((e) => (e as NamedTypeAnnotationCode).name.name)
            .toList();
        fieldGenerics[fieldName] = generics;
      }
    }

    final fieldTypesWithGenerics = fieldTypes.map(
      (name, type) {
        final generics = fieldGenerics[name];
        return MapEntry(
          name,
          generics == null ? type : '$type<${generics.join(', ')}>',
        );
      },
    );

    _buildFromJson(builder, className, fieldNames, fieldTypes, fieldGenerics);
    _buildToJson(builder, fieldNames, fieldTypes);
    _buildCopyWith(builder, className, fieldNames, fieldTypesWithGenerics);
    _buildToString(builder, className, fieldNames);
    _buildEquals(builder, className, fieldNames);
    _buildHashCode(builder, fieldNames);
  }

  void _buildFromJson(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
    Map<String, String> fieldTypes,
    Map<String, List<String>> fieldGenerics,
  ) {
    final code = [
      'factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),
      'return $className('.indent(4),
      for (final fieldName in fieldNames) ...[
        if (_baseTypes.contains(fieldTypes[fieldName])) ...[
          "$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},"
              .indent(6),
        ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
          "$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),
          '.whereType<Map<String, dynamic>>()'.indent(10),
          '.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),
          '.toList(),'.indent(10),
        ] else ...[
          '$fieldName: ${fieldTypes[fieldName]}'
                  ".fromJson(json['$fieldName'] "
                  'as Map<String, dynamic>),'
              .indent(6),
        ],
      ],
      ');'.indent(4),
      '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
  }

  void _buildToJson(
    MemberDeclarationBuilder builder,
    List<String> fieldNames,
    Map<String, String> fieldTypes,
  ) {
    final code = [
      'Map<String, dynamic> toJson() {'.indent(2),
      'return {'.indent(4),
      for (final fieldName in fieldNames) ...[
        if (_baseTypes.contains(fieldTypes[fieldName])) ...[
          "'$fieldName': $fieldName,".indent(6),
        ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
          "'$fieldName': $fieldName.map((e) => e.toJson()).toList(),".indent(6),
        ] else ...[
          "'$fieldName': $fieldName.toJson(),".indent(6),
        ],
      ],
      '};'.indent(4),
      '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
  }

  void _buildCopyWith(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
    Map<String, String> fieldTypes,
  ) {
    final code = [
      '$className copyWith({'.indent(2),
      for (final fieldName in fieldNames) ...[
        '${fieldTypes[fieldName]}? $fieldName,'.indent(4),
      ],
      '}) {'.indent(2),
      'return $className('.indent(4),
      for (final fieldName in fieldNames) ...[
        '$fieldName: $fieldName ?? this.$fieldName,'.indent(6),
      ],
      ');'.indent(4),
      '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
  }

  void _buildToString(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
  ) {
    final code = [
      '@override'.indent(2),
      'String toString() {'.indent(2),
      "return '$className('".indent(4),
      for (final fieldName in fieldNames) ...[
        if (fieldName != fieldNames.last) ...[
          "'$fieldName: \$$fieldName, '".indent(8),
        ] else ...[
          "'$fieldName: \$$fieldName'".indent(8),
        ],
      ],
      "')';".indent(8),
      '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
  }

  void _buildEquals(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
  ) {
    final code = [
      '@override'.indent(2),
      'bool operator ==(Object other) {'.indent(2),
      'return other is $className &&'.indent(4),
      'runtimeType == other.runtimeType &&'.indent(8),
      for (final fieldName in fieldNames) ...[
        if (fieldName != fieldNames.last) ...[
          '$fieldName == other.$fieldName &&'.indent(8),
        ] else ...[
          '$fieldName == other.$fieldName;'.indent(8),
        ],
      ],
      '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
  }

  void _buildHashCode(
    MemberDeclarationBuilder builder,
    List<String> fieldNames,
  ) {
    final code = [
      '@override'.indent(2),
      'int get hashCode {'.indent(2),
      'return Object.hash('.indent(4),
      'runtimeType,'.indent(6),
      for (final fieldName in fieldNames) ...[
        '$fieldName,'.indent(6),
      ],
      ');'.indent(4),
      '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
  }
}

extension on String {
  String indent(int length) {
    final space = StringBuffer();
    for (var i = 0; i < length; i++) {
      space.write(' ');
    }
    return '$space$this';
  }
}

こちらはサンプルプロジェクトの model.dart をもとに作成したものです。

非常に複雑に見えますが、実装しているのは以下の項目です。

  • FromJson
  • ToJson
  • CopyWith
  • ToString
  • Equals
  • HashCode

FromJson のコードを例にとってより詳しくみていきます。
FromJson ではデータモデルを作成したいクラスの className, fieldNames, fieldTypes, fieldGenerics を引数として受け取り、それらをもとに fromJson関数を作成しています。

  void _buildFromJson(
    MemberDeclarationBuilder builder,
    String className,
    List<String> fieldNames,
    Map<String, String> fieldTypes,
    Map<String, List<String>> fieldGenerics,
  ) {
    final code = [
      'factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),
      'return $className('.indent(4),
      for (final fieldName in fieldNames) ...[
        if (_baseTypes.contains(fieldTypes[fieldName])) ...[
          "$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},"
              .indent(6),
        ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
          "$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),
          '.whereType<Map<String, dynamic>>()'.indent(10),
          '.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),
          '.toList(),'.indent(10),
        ] else ...[
          '$fieldName: ${fieldTypes[fieldName]}'
                  ".fromJson(json['$fieldName'] "
                  'as Map<String, dynamic>),'
              .indent(6),
        ],
      ],
      ');'.indent(4),
      '}'.indent(2),
    ].join('\n');
    builder.declareInType(DeclarationCode.fromString(code));
  }

ここで以下のようなデータクラスが引数として渡された場合を考えます。

class User {
  final String name;
  final String email;

  User(this.name, this.email);

// この場合の FromJsonの引数
// className = User
// fieldNames = [ 'name', 'email' ]
// fieldTypes = { 'name': 'String', 'email': 'String' }

この時、FromJson の code の部分は以下のように変換されます。
これは通常のデータクラスを定義したときに一緒に定義する fromJson と同じような記述になります。

factory User.fromJson(Map<String, dynamic> json) {
  return User(
    name: json[name] as String,
    email: json[email] as String,
  );
}

つまり、この _buildFromJson メソッドで何をしているかというと、classNamefieldNames などのそれぞれのクラス独自の値を受け取り、その値を適切に代入することで今まで記述していた fromJson メソッドと同じようなメソッドを生成しているのです。

この辺りの実装については以下の記事が非常に参考になるかと思います。
https://www.sandromaglione.com/articles/macros-static-metaprogramming-and-primary-constructors-in-dart-and-flutter

_buildFromJson をもとに Macro の Model が何を行なっているかについてみましたが、そのほかの ToJsonCopyWith についても同様で、クラス独自の値を受け取り、今までの書き方のコードを生成しています。

2. 使用するデータモデルを作成

次に使用するデータモデルを作成していきます。
今回データを取得するのは JSONPlaceholder API です。
かなり単純な構造のデータを JSON 形式で取得することができます。
その中でも今回は postsphotos の二種類の取得を行いたいと思います。

データモデルのコードはそれぞれ以下の通りです。

post.dart
()
class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post({
    required this.userId,
    required this.id,
    required this.title,
    required this.body,
  });
}
photo.dart
()
class Photo {
  final int albumId;
  final int id;
  final String title;
  final String url;
  final String thumbnailUrl;

  Photo({
    required this.albumId,
    required this.id,
    required this.title,
    required this.url,
    required this.thumbnailUrl,
  });
}

一番初めの章で紹介したXの投稿まではシンプルにできませんでしたが、それでも非常にシンプルにデータモデルの定義ができています。
@Moddel() アノテーションをつけることで、先程 Macros を用いて作成した Model をクラスに付与することができます。

復習にはなりますが、 Post_buildFromJson 関数を例にとってみると、 _buildFromJson には以下のようなデータが引数として入ることになるかと思います。

className = Post
fieldNames = [ 'userId', 'id', 'title', 'body' ]
fieldTypes = { 'userId': 'int', 'id': 'int', 'title': 'String', 'body': 'String' }

これでモデルの定義は完了です。

3. APIからデータを取得して表示させる処理を実装

最後にAPIからデータを取得して表示させる処理を実装していきます。
コードは以下の通りです。

Post の場合

main.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
// import 'post.dartのパス';

Future main() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final response = await http.get(url);
  if (response.statusCode == 200) {
    final jsonList = jsonDecode(response.body) as List<dynamic>;
    const int limit = 10;
    for (int i = 0; i < limit; i++) {
      final json = jsonList[i];
      final post = Post.fromJson(json as Map<String, dynamic>);
      print('${post.id} --------------------');
      print('title: ${post.title}');
      print('body: ${post.body}');
      print('posted by id: ${post.userId}\n');
    }
  } else {
    print('Get Error');
  }
}

Photo の場合

main.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
// import 'photo.dartのパス';

Future main() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/photos');
  final response = await http.get(url);
  if (response.statusCode == 200) {
    final jsonList = jsonDecode(response.body) as List<dynamic>;
    const int limit = 10;
    for (int i = 0; i < limit; i++) {
      final json = jsonList[i];
      final photo = Photo.fromJson(json as Map<String, dynamic>);
      print('${photo.albumId} ---------------------');
      print('title: ${photo.title}');
      print('url: ${photo.url}');
      print('image url: ${photo.url}\n');
    }
  } else {
    print('Get Error');
  }
}

以下では Post を例にとって詳しくみていきます。

下記のコードではアクセスするAPIのエンドポイントの定義とデータの取得を行なっています。

final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(url);

以下では返ってきた JSONのリストに対して、取得する Post の件数を10件に絞って Post型に変換して表示しています。Post に関してはIDとタイトル、本文を print文で表示させるようにしています。

final jsonList = jsonDecode(response.body) as List<dynamic>;
const int limit = 10;
for (int i = 0; i < limit; i++) {
  final json = jsonList[i];
  final post = Post.fromJson(json as Map<String, dynamic>);
  print('${post.id} --------------------');
  print('title: ${post.title}');
  print('body: ${post.body}');
  print('posted by id: ${post.userId}\n');
}

これでデータクラスの定義からデータ取得、表示までの一連の実装は完了です。

4. 実行と結果

最後に今までのコードを実行するのですが、記事の冒頭にも記述した通り、2024年4月5日現在 Dart の Macros は Dart では実行できるものの、 Flutter では使用することができません。したがって、 flutter run ではなく別の方法を取る必要があります。

ターミナルを開き、以下のコマンドを実行することで Macros を含む Dart の実行を行うことができます。

dart --enable-experiment=macros [実行するファイルのパス]

// lib 直下の main.dart に main() がある場合
dart --enable-experiment=macros lib/main.dart

これで実行すると結果はそれぞれ以下のようになります。(長いので折りたたんでいます)

Post
1 --------------------
title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
body: quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto
posted by id: 1

2 --------------------
title: qui est esse
body: est rerum tempore vitae
sequi sint nihil reprehenderit dolor beatae ea dolores neque
fugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis
qui aperiam non debitis possimus qui neque nisi nulla
posted by id: 1

3 --------------------
title: ea molestias quasi exercitationem repellat qui ipsa sit aut
body: et iusto sed quo iure
voluptatem occaecati omnis eligendi aut ad
voluptatem doloribus vel accusantium quis pariatur
molestiae porro eius odio et labore et velit aut
posted by id: 1

4 --------------------
title: eum et est occaecati
body: ullam et saepe reiciendis voluptatem adipisci
sit amet autem assumenda provident rerum culpa
quis hic commodi nesciunt rem tenetur doloremque ipsam iure
quis sunt voluptatem rerum illo velit
posted by id: 1

5 --------------------
title: nesciunt quas odio
body: repudiandae veniam quaerat sunt sed
alias aut fugiat sit autem sed est
voluptatem omnis possimus esse voluptatibus quis
est aut tenetur dolor neque
posted by id: 1

6 --------------------
title: dolorem eum magni eos aperiam quia
body: ut aspernatur corporis harum nihil quis provident sequi
mollitia nobis aliquid molestiae
perspiciatis et ea nemo ab reprehenderit accusantium quas
voluptate dolores velit et doloremque molestiae
posted by id: 1

7 --------------------
title: magnam facilis autem
body: dolore placeat quibusdam ea quo vitae
magni quis enim qui quis quo nemo aut saepe
quidem repellat excepturi ut quia
sunt ut sequi eos ea sed quas
posted by id: 1

8 --------------------
title: dolorem dolore est ipsam
body: dignissimos aperiam dolorem qui eum
facilis quibusdam animi sint suscipit qui sint possimus cum
quaerat magni maiores excepturi
ipsam ut commodi dolor voluptatum modi aut vitae
posted by id: 1

9 --------------------
title: nesciunt iure omnis dolorem tempora et accusantium
body: consectetur animi nesciunt iure dolore
enim quia ad
veniam autem ut quam aut nobis
et est aut quod aut provident voluptas autem voluptas
posted by id: 1

10 --------------------
title: optio molestias id quia eum
body: quo et expedita modi cum officia vel magni
doloribus qui repudiandae
vero nisi sit
quos veniam quod sed accusamus veritatis error
posted by id: 1
Photo
1 ---------------------
title: accusamus beatae ad facilis cum similique qui sunt
url: https://via.placeholder.com/600/92c952
image url: https://via.placeholder.com/600/92c952

1 ---------------------
title: reprehenderit est deserunt velit ipsam
url: https://via.placeholder.com/600/771796
image url: https://via.placeholder.com/600/771796

1 ---------------------
title: officia porro iure quia iusto qui ipsa ut modi
url: https://via.placeholder.com/600/24f355
image url: https://via.placeholder.com/600/24f355

1 ---------------------
title: culpa odio esse rerum omnis laboriosam voluptate repudiandae
url: https://via.placeholder.com/600/d32776
image url: https://via.placeholder.com/600/d32776

1 ---------------------
title: natus nisi omnis corporis facere molestiae rerum in
url: https://via.placeholder.com/600/f66b97
image url: https://via.placeholder.com/600/f66b97

1 ---------------------
title: accusamus ea aliquid et amet sequi nemo
url: https://via.placeholder.com/600/56a8c2
image url: https://via.placeholder.com/600/56a8c2

1 ---------------------
title: officia delectus consequatur vero aut veniam explicabo molestias
url: https://via.placeholder.com/600/b0f7cc
image url: https://via.placeholder.com/600/b0f7cc

1 ---------------------
title: aut porro officiis laborum odit ea laudantium corporis
url: https://via.placeholder.com/600/54176f
image url: https://via.placeholder.com/600/54176f

1 ---------------------
title: qui eius qui autem sed
url: https://via.placeholder.com/600/51aa97
image url: https://via.placeholder.com/600/51aa97

1 ---------------------
title: beatae et provident et ut vel
url: https://via.placeholder.com/600/810b14
image url: https://via.placeholder.com/600/810b14

結果にある通り、それぞれの独自のクラスに関して、JSON形式から正常に変換して取得、表示できていることがわかります。

今後の可能性

Macros は現在 Dart のみで実行が可能になっていますが、 Pub.dev を確認したところ以下のように Dart Team によって開発されている Macros パッケージが確認できました。
Flutter で使用できるようになる日も近いかもしれません。

https://pub.dev/packages/macros

まとめ

最後まで読んでいただいてありがとうございました。

Macros を触ってみて、Flutterに導入されれば開発効率が大きく上がる技術であると感じました。
今回作成したデータクラスは二つのみでしたが、作成した Macros はアノテーションとして使いまわせるた、扱うデータクラスが増えれば増えるほど、他の書き方と比較して楽に記述できると感じました。

一方で Macros を使用して手軽にかけてしまうことで、もちろん使い方次第ではありますが、Modelなどの内部で何が起こっているのかを意識しなくなり、機能追加や変更に対処できなくなる可能性もあると感じました。
例えば今回の例だと Macros を使っているデータクラスのプロパティが nullable の時の実装はどうするのかなど、すでにあるマクロだけでなく自身で変更を加えるなどしてどのような処理が行われているかは把握しておく必要があると感じました。

誤っている点やもっと良い書き方があればご指摘いただければ幸いです。

参考

https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md

https://github.com/millsteed/macros

https://pub.dev/packages/macros

https://www.sandromaglione.com/articles/macros-static-metaprogramming-and-primary-constructors-in-dart-and-flutter

https://github.com/millsteed/macros/tree/main

引用させていただいた投稿
https://x.com/SandroMaglione/status/1752682717563568419?s=20

https://x.com/spydon/status/1752629743222993184

Discussion