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 isDeleted
,isBool
将返回true
。而对于源final String isDeleted
,isBool
将返回false
。 - 领域 - 包含系统。例如,
OfflineFirstWithRest
领域在适配器内构建REST序列化和SQLite序列化,并通过其自身的注解发现。
API注意事项
Brick是一个有意见的库,无论提供者或领域如何,都提供一致且可预测的交互是该项目的主要目标。实现以下指南不是必需的,但在构建自定义提供者和领域时,请务必考虑这些指南。
提供者
类级别配置
虽然模型不应知道提供者,但提供者的配置可能被存储库使用或向适配器提供必要信息。由于这是通过注解访问的,配置必须是const
。类级别的配置可用于设置默认值,描述依赖于实例的行为:
RestSerializable(
fieldName: FieldRename.pascal,
nullable: true,
)
配置还可以描述依赖于实例的行为。由于不能在const
类中传递函数,因此可以使用const
化的函数主体:
RestSerializable(
requestTransformer: UserRequestTransformer.new
)
强烈建议不要要求最终实现声明字符串化函数的参数。如果提供者的属性参数发生变化,Dart类型系统不会在运行时之前检测到错误。
⚠️ 如果提供者与dart:mirrors
的使用冲突,则应将配置托管在独立的包中。例如,brick_sqlite
和brick_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,
},
};
}
适配器 - 由序列化和反序列化代码以及自定义翻译映射(如fieldsToSqliteColumns
或restEndpoint
)组成 - 是使用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']}")
关联
关联可能需要复杂的查找。当领域支持提供者之间的关联时,类级别注解应被用于自定义检查器。例如,isSibling
或isAssociation
。
建议使用存储库方法专门用于关联查找,而不是提供者,因为存储库可能会将查找路由到不同的提供者。例如,一个用户可能有一顶帽子,而存储库可能已经在这个内存提供者中拥有那顶帽子。通过请求存储库,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
更多关于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
一起使用,以生成代码。以下是一个基本的使用步骤:
-
创建 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'], }; }
-
注册 Builder: 在
build.yaml
文件中注册你的 Builder。builders: my_builder: import: "package:my_package/builder.dart" builder_factory: "MyBuilder" build_extensions: {".dart": [".g.dart"]} auto_apply: dependencies
-
运行
build_runner
: 使用build_runner
来生成代码。flutter pub run build_runner build
你也可以使用
watch
命令来持续监听文件变化并自动生成代码:flutter pub run build_runner watch
示例
假设你想为每个类生成一个 toString
方法,你可以这样做:
-
创建 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'], }; }
-
注册 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
-
运行
build_runner
:flutter pub run build_runner build