Flutter构建工具插件brick_build的使用

Flutter构建工具插件brick_build的使用

概览

本文档将详细介绍如何使用Flutter构建工具插件brick_build来生成适配器和模型字典。我们将通过完整的示例来演示这一过程。

设置

在编辑模型时,推荐使用watch模式:

(flutter) pub run build_runner watch

如果你不使用watch模式,务必运行两次build命令,以便第一次运行时能够检测到新的迁移:

(flutter) pub run build_runner build

一个应用程序目录应该如下所示:

| my-app
|--lib
|--|--app
|--|--|--adapters
|--|--|--models

这确保了可以一致地访问子数据(例如模型)。

目录

术语表

  • 生成器 - 代码生产者。生成器的输出通常是将输入转换为规范化数据的函数。生成器的输出并不总是构成一个完整的文件(例如,一个生成器是序列化器,另一个生成器是反序列化器,两者组合成一个超级适配器生成器)。
  • 构建器 - 一个类,在编写生成代码到文件之前介于源文件和生成器之间。它们由build.yaml调用和配置。构建器主要关注源中存在的注解(例如,Flutter应用)。
  • 序列化/反序列化 - 简写自serialize/deserialize。
  • 检查器 - 一个可访问的工具,用于从源中分析类型并进行类型检查。例如,对于源final bool isDeletedisBool将返回true。而对于源final String isDeletedisBool将返回false
  • 领域 - 包含系统。例如,OfflineFirstWithRest领域在适配器内构建REST序列化和SQLite序列化,并通过其自身的注解发现。

API注意事项

Brick是一个有意见的库,无论提供者或领域如何,都提供一致且可预测的交互是该项目的主要目标。实现以下指南不是必需的,但在构建自定义提供者和领域时,请务必考虑这些指南。

提供者

类级别配置

虽然模型不应知道提供者,但提供者的配置可能被存储库使用或向适配器提供必要信息。由于这是通过注解访问的,配置必须是const。类级别的配置可用于设置默认值,描述依赖于实例的行为:

RestSerializable(
  fieldName: FieldRename.pascal,
  nullable: true,
)

配置还可以描述依赖于实例的行为。由于不能在const类中传递函数,因此可以使用const化的函数主体:

RestSerializable(
  requestTransformer: UserRequestTransformer.new
)

强烈建议不要要求最终实现声明字符串化函数的参数。如果提供者的属性参数发生变化,Dart类型系统不会在运行时之前检测到错误。

⚠️ 如果提供者与dart:mirrors的使用冲突,则应将配置托管在独立的包中。例如,brick_sqlitebrick_sqlite_abstract

字段级别配置

提供者可以选择使用注解在字段级别实现配置。字段级注解可能用于覆盖更细粒度的行为。这些注解应该实现FieldSerializable

@Rest(
  // 这里的属性可能会覆盖在类级别预先指定的行为
  name: "deleted"
)
final bool isDeleted;

@Rest(ignore: true, name: "e-mail")
@Sqlite(unique: true)
final String email;

由于字段级注解是最常编写的注解,因此它们具有最易读的名称。字段级注解名称的约定仅仅是提供者名称减去"Provider"。

💡 保持注解尽可能原子化。一个提供者注解对另一个提供者不可靠。

查询

每个公共实例方法都应该支持名为{Query query}的命名参数。Query是应用程序与抽象提供者或存储库之间的粘合剂。它被存储库和提供者访问,但由于是最后一步,提供者应该以最基本的形式解释Query

limit

提供者应从源返回结果的最大数量:

Query(limit: 10)
offset

提供者搜索结果的起始索引:

Query(offset: 10)
forProviders

可用参数可以根据提供者而有所不同;这允许实现查询特定来源的唯一语句。

where

where查询模型的属性。提供者可以选择性地支持where参数。例如,虽然SQLite提供者始终支持列查询,但RESTful API可能不太一致,可能需要按摩字段名:

[Where('firstName').isExactly('Thomas'), Where('age').isExactly(42)];
// SQLite => SELECT * FROM Users WHERE first_name = "Thomas" AND age = 42;
// REST => https://api.com/users?by_first_name=Thomas&age=42

从模型字段名(例如firstName)到序列化字段名(例如first_name)的转换可以在适配器中或在类级别配置(例如RestSerializable#endpoint)中发生。然而,它应该始终由提供者从适配器中访问。

适配器

提供者从其源接收原始数据后,必须将其构建为模型或模型列表。这种转换发生在适配器中。首先,适配器通过模型字典被发现,这是一个简单的哈希表,连接模型和适配器:

Future<_Model> get<_Model extends RestModel>({Query query}) async {
  // 连接到_ModelAdapter
  final adapter = modelDictionary.forAdapter[_Model];
  final resp = ... // 从HTTP获取响应

  // 现在提供者可以序列化/反序列化
  return response.map((r) => adapter.fromRest(r));
}

适配器也可以在提供者中促进反序列化,带有有关类的其他信息:

class UserAdapter {
  // 类级别配置可以复制到适配器
  final String fromKey = "users";

  // 将字段名(由Query#where提供)转换为其SQLite列名
  final fieldsToSqliteColumns = {
    "primaryKey": {
      "name": "_brick_id",
      "type": int,
      // 一些关于类型的其他信息在构建后不再可用,
      // 因为这需要镜子,但是它可以保留在适配器中
      "iterable": false,
      "association": false,
    },
  };
}

适配器 - 由序列化和反序列化代码以及自定义翻译映射(如fieldsToSqliteColumnsrestEndpoint)组成 - 是使用brick_build生成的。

⚠️ 提供者不应依赖于由另一个提供者库生成的适配器代码。

模型

当创建一个提供者依赖的模型时,仅声明提供者使用的成员。这些成员应在提供者的生态系统中被视为受保护:它们在最终实现中的使用应受到鼓励。

abstract class SqliteModel {
  // 提供者依赖于主键与其他模型建立关联
  int primaryKey;
}

存储库

在设计接口之前,请先查看描述最佳实践方法的文档创建自定义存储库。构建库不应该生成存储库,但有时领域会添加额外的配置,需要扩展提供者生成器(例如,OfflineFirstSerdes)。

类级别配置

模型注解应命名为Connect<DOMAIN>,包括提供者配置,并且不应操作其他提供者使用的配置。如果配置对存储库是必需的,它应只在存储库级别相关:

// BAD
@ConnectOfflineFirstWithRest(
  fieldRename: FieldRename.snake,
  restConfig: RestSerializable(
    // 在两个地方声明相同的配置
    // 没有明确的逻辑选择层次结构
    fieldRename: FieldRename.pascal,
  )
)

// GOOD
@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(
    fieldRename: FieldRename.pascal,
  ),
  sqliteConfig: SqliteSerializable(
    ignore: true,
  ),
  // 这个属性将影响所有与模型的交互
  alwaysHydrate: true,
)

字段级别配置

注解应仅反映与存储库相关的配置(例如合成指令)。它们不应是捷径:

// BAD:
@OfflineFirst(ignore: true)

// GOOD:
@Rest(ignore: true)
@Sqlite(ignore: true)

注解在以下情况下最有用:其明确目的结合多个提供者 并且

@OfflineFirst(where: "{'email': data['email']}")

与原子提供者注解不同,存储库 可以并且应该 访问所有相关的提供者注解:

// 当生成适配器代码时,这三个注解对OfflineFirst领域都有用
@Rest(name: 'LastName')
@Sqlite(name: 'last_name')
@OfflineFirst(where: "{'last_name': data['LastName']}")

关联

关联可能需要复杂的查找。当领域支持提供者之间的关联时,类级别注解应被用于自定义检查器。例如,isSiblingisAssociation

建议使用存储库方法专门用于关联查找,而不是提供者,因为存储库可能会将查找路由到不同的提供者。例如,一个用户可能有一顶帽子,而存储库可能已经在这个内存提供者中拥有那顶帽子。通过请求存储库,SqliteProvider就可以避免一次昂贵的查询。

代码生成

在进一步阅读之前,请注意这个过程似乎需要很多代码。这主要是类型检查和Dart分析器所需的模板代码。大多数自定义代码和逻辑将存在于适配器序列化中。

包装设置

如果注解和配置定义依赖于与镜子冲突的包(例如,Flutter与镜子冲突),则必须在构建包之外声明它们。由于其他包可能会使用这些注解(例如,OfflineFirst考虑@Rest@Sqlite注解以及@OfflineFirst),因此最好将注解和构建器保持为独立包。

例如,Cloud Firestore包:

brick_cloud_firestore
|--example
|--README.md
|--packages

// 此包是导入到dependencies: pubspec.yaml中的唯一包
// 它包含可导入的提供者和存储库
// 存储库也可能选择从姐妹包导出注解
|--|--brick_cloud_firestore

// 由于firestore依赖于sqlite,因此一个抽象包包括
// 注解和类级别配置类,可以被brick_build消化
// 和brick_cloud_firestore包
|--|--brick_cloud_firestore_abstract

// 专属于提供者的代码生成。导出序列化、反序列化、字段、提供者模型序列化和任何其他生成器
|--|--brick_cloud_firestore_generators

// 输出和保存生成的代码到磁盘。此包是唯一包含build.yaml的包
// 每个构建包只能发现一个注解
|--|--brick_offline_first_with_cloud_firestore_build
  • ❌ 如果提供者有Flutter依赖,则注解和配置的单独包作为_abstract包存在
  • _generators不包含 build.yaml(多个build.yaml文件可能导致竞争碰撞)
  • <Provider>Fields<Provider>SerializeGenerator<Provider>DeserializeGenerator<Provider>ModelSerdesGenerator可以从_generators包外访问
  • ❌ 每个_build包只能发现一个类级别注解

提供者

类级别配置

提供者可能需要关于类的高层次信息,这些信息不适合在类的每个实例上定义。为此,提供者可以声明类级别配置:

RestSerializable(
  // REST端点不适合定义为实例级定义
  requestTransformer: UserRequestTransformer.new,
  // 类级别配置也适用于在类中设置后续字段级定义的默认值
  fieldRename: FieldRename.snake,
)

这些配置可能直接注入适配器(如endpoint)或可能改变生成代码的行为(如fieldRename)。

该类 不应 用作注解。相反,它是通过域发现的类级别注解的成员接收的。

解析类级别配置

一旦类被构建器发现,配置将从注解中拉出并扩展为易于消化的Dart形式:

// RestSerializable是我们之前提到的配置类
class RestModelSerdesGenerator extends ProviderSerializableGenerator<RestSerializable> {
  RestModelSerdesGenerator(Element element, ConstantReader reader)
        // 后续消费者必须在他们的类级别域注解中使用此配置键
        // 或任何用于发现模型类的注解
      : super(element, reader, configKey: "restConfig");

  get config {
    if (reader.read(configKey).isNull) return RestSerializable.defaults;

    return RestSerializable(
      // withinConfigKey安全地导航常量化值,解释为易于消化的Dart代码
      endpoint: withinConfigKey("endpoint")?.stringValue ?? RestSerializable.defaults.endpoint,
    );
  }
}

发现和解析字段级别注解

类似地,字段级别注解必须从其常量化版本恢复为易于消化的形式。Brick提供了此类的基础类:

// @Rest是我们的注解和字段级别配置类,通过AnnotationFinder<Rest>声明
class RestAnnotationFinder extends AnnotationFinder<Rest> {
  // 这是之前定义的类级别配置
  final RestSerializable config;

  RestAnnotationFinder([this.config]);

  // element是字段,例如`final bool isDeleted`
  from(element) {
    // objectForField将分析器的原始数据转换为可管理的代码
    final obj = objectForField(element);

    // 如果这个字段是
    // final bool isDeleted
    // 而不是
    // @Rest(ignore:)
    // final bool isDeleted
    // 那么我们将使用默认值生成配置
    if (obj == null) {
      return Rest(
        ignore: Rest.defaults.ignore
      );
    }

    // 最后,我们将注解的配置重新转换为易于消化的Dart代码
    return Rest(
      ignore: obj.getField('ignore').toBoolValue() ?? Rest.defaults.ignore,
    );
  }
}

这在字段级别重新初始化。然而,类将需要所有字段都经过相同的过程,因此必须制作FieldsForClass类。这些字段将传递给我们的(反)序列化生成器:

// @Rest仍然是我们的注解
// 这个类是样板代码,可以安全地复制并根据类型进行更改
class RestFields extends FieldsForClass<Rest> {
  final RestAnnotationFinder finder;
  final RestSerializable config;

  RestFields(ClassElement element, [RestSerializable this.config])
      : finder = RestAnnotationFinder(config),
        super(element: element);
}
``

对于不使用类级别配置的提供者,`Fields`实现可以调整:

```dart
class RestFields extends FieldsForClass<Rest> {
  final finder = RestAnnotationFinder();

  RestFields(ClassElement element) : super(element: element);
}

适配器

适配器序列化生成器应尽可能原子化,不应期望其他适配器生成器的代码。通过继承SerdesGenerator_generators包可以快速生成序列化函数,供后续在生成器中使用:

// FieldSerializable是在brick_core中定义的字段级别注解协议
abstract class RestSerializeGenerator extends SerdesGenerator<Rest, RestModel> {
  final providerName = 'Rest';
  final doesDeserialize = false;

  RestSerialize(ClassElement element, RestFields fields) : super(element, fields);
}
``

每个未忽略的模型字段都将通过`coderForField`函数的最终函数。此函数根据可用的配置提供字段级别的代码生成:

```dart
class RestSerialize extends OfflineFirstGenerator<Rest> {
  // 所有发现的类字段都将通过此函数进行生成输出
  // 私有字段、方法、静态成员和计算的setter自动忽略
  String coderForField(field, checker, {wrappedInFuture, fieldAnnotation}) {
    // 在序列化时,字段值将是`instance.fieldName`
    // 在反序列化时,字段值将是`data['field_name']`
    final fieldValue = serdesValueForField(field, fieldAnnotation.name, checker: checker);
    final defaultValue = SerdesGenerator.defaultValueSuffix(fieldAnnotation);

    if (checker.isString) {
      return fieldValue;
    }

    if (checker.isDateTime) {
      return '$fieldValue?.toIso8601String()';
    }

    // 落入不支持的类型,null不会添加到生成的输出
    return null;
  }
}
``

适配器始终包括提供者的序列化和反序列化方法。至少,所有基本类型应由检查器评估,并返回适当的序列化或反序列化代码给生成器。序列化生成器会产生代码“面条”, **这没问题**。显式、冗长的声明即使在跨生成器重复出现也是可靠的,并且易于调试。

适配器还可以包括有用的信息,例如SQLite提供者的模式数据或REST提供者的端点生成函数。提供者可以且应该通过适配器访问通用信息(即与特定模型实例无关的模型信息)。适配器成员,像模型一样,只有在提供者使用时才应声明。

```dart
// 适配器方法和字段在<PROVIDER>(De)serializeGenerators中声明
List<String> get instanceFieldsAndMethods {
  var toKey = (fields as RestFields).config?.toKey?.trim();

  // 字符串应该始终包裹,因为这是生成代码
  // 它看起来不自然,类型系统也不会捕捉错误,
  // 所以一定要写全面的测试
  if (toKey != null) toKey = "'$toKey'";

  return ['final String toKey = $toKey;'];
}
``

最后,序列化和反序列化生成器应分别放在不同的类中以提高可读性:

```dart
class RestSerializeGenerator extends SerdesGenerator<Rest, RestModel> {
  final doesDeserialize = false;
}

class RestDeserialize extends SerdesGenerator<Rest, RestModel> {
  final doesDeserialize = true;
}
``

#### 调用生成器

(反)序列化生成器通过之前的`<PROVIDER>ModelSerdesGenerator`访问:

```dart
class RestModelSerdesGenerator extends ProviderSerializableGenerator<RestSerializable> {
  ...

  @override
  get generators {
    final classElement = element as ClassElement;
    // 前面扩展的配置现在传递给我们的Fields类
    final fields = RestFields(classElement, config);
    return [
      // 这些生成器的输出将在稍后的步骤中通过构建器访问
      RestDeserialize(classElement, fields),
      RestSerialize(classElement, fields),
    ];
  }
}
``

#### 领域

##### 类级别注解

领域注解在类级别被领域构建器发现。 **每个构建包只能发现一个类级别注解**。注解包括领域内每个提供者的配置选项:

```dart
@ConnectOfflineFirstWithRest(
  // RestSerializable是我们的配置主体。
  restConfig: RestSerializable(
    requestTransfomer: MyModelTransformer.new,
    fieldRename: FieldRename.snake,
  )
)
class MyModel
``

###### 解析类级别注解

由于提供者的配置解释由`<PROVIDER>ModelSerdesGenerator`处理,将注解转发给生成器就足够了。

⚠️ 在声明注解接口时,请确保注解键名与提供者的`<PROVIDER>ModelSerdesGenerator#configKey`相匹配。否则,构建包将不得不子类化模型生成器以锁定其配置键。

###### 发现类级别注解

`AnnotationSuperGenerator`管理子生成器。此生成器是其他生成器的入口点。它应该很简单,大部分逻辑委托给子生成器。

```dart
// @ConnectOfflineFirstWithRest是装饰我们领域模型的注解
class OfflineFirstGenerator extends AnnotationSuperGenerator<ConnectOfflineFirstWithRest> {
  // 适配器输出所需
  final String superAdapterName;

  const OfflineFirstGenerator({
    this.superAdapterName = 'OfflineFirst',
  });

  /// 给定[element]和(annotation),生成生成器
  List<SerdesGenerator> buildGenerators(Element element, ConstantReader annotation) {
    // `RestModelSerdesGenerator从`ConnectOfflineFirstWithRest`注解中获取其配置
    final rest = RestModelSerdesGenerator(element, annotation);
    final sqlite = SqliteModelSerdesGenerator(element, annotation);
    return <SerdesGenerator>[]
        ..addAll(rest.generators)
        ..addAll(sqlite.generators);
  }
}
``

此类将在其他生成器中使用,如[构建步骤](#builder)所述。

#### 模型字典 (brick.g.dart)

模型字典生成器必须为 **每个** 提供者生成模型字典。定义说明(例如,不提交生成的代码)和引导代码注释(例如,映射的内容)很重要但非必需。

由于每个模型应该扩展/实现每个提供者的模型类型,每个适配器应该扩展/实现每个提供者的适配器类型,相同的字典用于每个提供者映射:

```dart
// 这个方法是从超类继承的
final dictionary = dictionaryFromFiles(classNamesToFileNames);

return """
/// REST映射仅在初始化RestProvider时使用
final Map<Type, RestAdapter<RestModel>> restMappings = {
  $dictionary
};
final restModelDictionary = RestModelDictionary(restMappings);

/// Sqlite映射仅在初始化SqliteProvider时使用
final Map<Type, SqliteAdapter<SqliteModel>> sqliteMappings = {
  $dictionary
};
final sqliteModelDictionary = SqliteModelDictionary(sqliteMappings);
""";
``

为了支持映射,每个适配器都必须作为`part`包含,每个模型都必须作为导入包含:

```dart
// 这些方法是从超类继承的
final adapters = adaptersFromFiles(classNamesToFileNames);
final models = modelsFromFiles(classNamesToFileNames);

return """
$models

$adapters
""";
``

在适配器中使用的任何导入也必须导入:

```dart
return """
import 'dart:convert';
import 'package:brick_sqlite/db.dart' show SqliteModel, SqliteAdapter, SqliteModelDictionary;
import 'package:brick_rest/brick_rest.dart' show RestProvider, RestModel, RestAdapter, RestModelDictionary;
// ignore: unused_import, unused_shown_name
import 'package:sqflite/sqflite.dart' show DatabaseExecutor;
""";
``

💡 为了减少分析器错误,请包含`// ignore: unused_import`用于部分文件中的导入。

#### 构建器

最后,生成的代码通过`builder.dart`文件输出到磁盘。

主要的构建功能将是适配器和模型字典,因为这些是Brick系统的关键:

```dart
final offlineFirstGenerator = const OfflineFirstGenerator();

// 所有模型必须聚合到一个文件以检查关联
Builder offlineFirstAggregateBuilder(options) => AggregateBuilder(requiredImports: [
      "import 'package:brick_offline_first/brick_offline_first.dart';",
      "import 'package:brick_sqlite/db.dart';",
    ]);

// 适配器构建器使用OfflineFirstGenerator中声明的相同注解
// 它还依赖于`buildGenerators`函数
Builder offlineFirstAdaptersBuilder(options) =>
    AdapterBuilder<ConnectOfflineFirstWithRest>(offlineFirstGenerator);

// 模型字典构建器同样需要领域的类级别注解
// 此构建将执行可选清理
Builder offlineFirstModelDictionaryBuilder(options) =>
    ModelDictionaryBuilder<ConnectOfflineFirstWithRest>(
      const OfflineFirstModelDictionaryGenerator(),
      expectedImportRemovals: [
        "import 'package:brick_offline_first/brick_offline_first.dart';",
        'import "package:brick_offline_first/brick_offline_first.dart";',
      ],
    );
``

生成器通过[在`builders.dart`中的]构建器调用,构建器通过Dart的原生任务运行器由`build.yaml`调用。由于`build.yaml`对初学者来说可能是晦涩难懂的,并且不属于本仓库的一部分,关于定制的文档可以在[该包页面](https://pub.dartlang.org/packages/build_config)上找到。对于基本的、经过验证的使用,可以使用[本仓库的build.yaml](https://github.com/GetDutchie/brick/blob/main/packages/brick_build/build.yaml)作为基础,并适当修改以适应自定义领域。

### 测试

使用`lib/testing.dart`辅助工具可以比较生成的代码与预期输出。

要生成的代码的源必须保存在与测试套件分开的文件中:

```dart
// test/generated_source/test_simple.dart
@ConnectMyDomain()
class User extends MyDomainModel {}

// 为了便于发现,建议将输出包含在同一文件中
final output = r'''
class MyDomainAdapter....
''';
``

在测试套件中,可以编写期望:

```dart
import 'package:brick_build_test/brick_build_test.dart';
import 'generated_source/test_simple.dart' as _$simple;

final generator = MyDomainGenerator();

test('simple', () {
  final annotation = await annotationForFile<ConnectOfflineFirstWithRest>('generated_source', 'simple');
  final generated = await (generator ?? _generator).generateAdapter(
    annotation?.element,
    annotation?.annotation,
    null,
  );
  expect(generated, _$simple.output);
});
``

由于适配器通常包括与序列化无关的多余代码(例如提供者的支持信息),测试的范围可以缩小到仅(反)序列化代码:

```dart
final generateReader = generateLibraryForFolder('generated_source');
test('simple', () {
  final reader = await generateReader('simple');
  final generated = await generator.generate(reader, null);
  expect(generated, _$simple.output);
});
``

### 高级技术

#### 自定义类型检查

大多数生成器可能不需要扩展基本类型检查(这是字符串吗?这是整数吗?这是列表吗?)。对于高级检查(例如发现特定包的类,如`OfflineFirstSerdes`),可能需要创建一个新的检查器:


更多关于Flutter构建工具插件brick_build的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter构建工具插件brick_build的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


brick_build 是一个 Flutter 构建工具的插件,用于生成代码、自动化任务和扩展 Flutter 的构建过程。它通常用于在开发过程中自动生成一些重复性的代码或配置文件,从而提高开发效率。

安装 brick_build

要使用 brick_build,首先需要在 pubspec.yaml 文件中添加依赖项:

dev_dependencies:
  brick_build: ^1.0.0

然后运行 flutter pub get 来安装依赖。

基本用法

brick_build 通常与 build_runner 一起使用,以生成代码。以下是一个基本的使用步骤:

  1. 创建 Builder: 你需要创建一个继承自 Builder 的类,并实现 build 方法。这个方法用于定义生成代码的逻辑。

    import 'package:build/build.dart';
    import 'package:source_gen/source_gen.dart';
    
    class MyBuilder extends Builder {
      @override
      Future<void> build(BuildStep buildStep) async {
        // 生成代码的逻辑
      }
    
      @override
      Map<String, List<String>> get buildExtensions => {
            '.dart': ['.g.dart'],
          };
    }
    
  2. 注册 Builder: 在 build.yaml 文件中注册你的 Builder。

    builders:
      my_builder:
        import: "package:my_package/builder.dart"
        builder_factory: "MyBuilder"
        build_extensions: {".dart": [".g.dart"]}
        auto_apply: dependencies
    
  3. 运行 build_runner: 使用 build_runner 来生成代码。

    flutter pub run build_runner build
    

    你也可以使用 watch 命令来持续监听文件变化并自动生成代码:

    flutter pub run build_runner watch
    

示例

假设你想为每个类生成一个 toString 方法,你可以这样做:

  1. 创建 Builder:

    import 'package:build/build.dart';
    import 'package:source_gen/source_gen.dart';
    
    class ToStringBuilder extends Builder {
      @override
      Future<void> build(BuildStep buildStep) async {
        final assetId = buildStep.inputId;
        final sourceCode = await buildStep.readAsString(assetId);
    
        final output = '''
        extension \$${assetId.pathSegments.last}ToString on ${assetId.pathSegments.last} {
          @override
          String toString() {
            return '${assetId.pathSegments.last}()';
          }
        }
        ''';
    
        final outputId = assetId.changeExtension('.g.dart');
        await buildStep.writeAsString(outputId, output);
      }
    
      @override
      Map<String, List<String>> get buildExtensions => {
            '.dart': ['.g.dart'],
          };
    }
    
  2. 注册 Builder:

    build.yaml 中注册 ToStringBuilder

    builders:
      to_string_builder:
        import: "package:my_package/builder.dart"
        builder_factory: "ToStringBuilder"
        build_extensions: {".dart": [".g.dart"]}
        auto_apply: dependencies
    
  3. 运行 build_runner:

    flutter pub run build_runner build
回到顶部