Dart笔记build_runner用于 Dart 代码生成和模块化编译的构建系统
1. 概述
2.1 build_runner 用于解决什么问题
build_runner
库是为了解决 Dart 和 Flutter 中代码生成的问题而创建的,用于生成代码。代码生成是一种常见的编程技术,它可以帮助开发者自动化一些重复或模板化的编程任务,从而提高开发效率和代码质量。
在 Dart 和 Flutter 中,有许多场景可能需要使用到代码生成。例如:
- 序列化 和 反序列化:对于复杂的数据结构,手动编写序列化和反序列化的代码可能会非常繁琐和容易出错。通过使用代码生成,我们可以自动地为数据结构生成序列化和反序列化的代码。
- ORM(对象关系映射):在处理数据库时,我们通常需要将数据库中的表映射到Dart的对象。这个映射过程可以通过代码生成来自动化。
- 依赖注入:依赖注入是一种常见的设计模式,它可以帮助我们更好地组织和管理代码中的依赖关系。通过使用代码生成,我们可以自动地为依赖注入生成所需的代码。
然而,代码生成也有其挑战。其中一个主要的挑战是 处理文件和依赖关系。例如,当一个文件发生变化时,我们可能需要重新生成依赖于这个文件的所有其他文件的代码。build_runner 提供一个强大且灵活的方式来管理和自动化代码生成过程中的文件和依赖关系。
2.2 build_runner 的安装
使用 build_runner,你需要在你的 pubspec.yaml
文件中添加指定版本的 build_runner 作为开发依赖项。需要指出的是,通常,将它放在pubspec.yaml的dev_dependencies下:
dev_dependencies: build_runner:
然后,你可以在命令行中运行build_runner:
dart run build_runner build
这将运行所有配置的生成器,并将生成的代码输出到指定的目录。
注:当提供构建器的包通过 build.yaml 文件配置时,它们被设计为使用生成的构建脚本进行消费。大多数构建器应该需要很少或不需要配置,具体取决于构建器的文档。如果你需要将web代码编译为js,需要在 dev_dependencies 中添加 build_web_compilers。
2. build_runner 的用法介绍
2.1 内置命令和选项
除了 build
命令外, build_runner还提供了其它的内置命令,如 watch
、serve
等。其中:
命令 | 描述 |
build |
运行一次构建并退出 |
watch |
运行一个持久的构建服务器,监视文件系统的编辑并在必要时进行重建 |
serve |
与 watch 相同,但同时运行一个开发服务器。默认情况下,它在 8080 和 8081 端口上分别提供 web 和 test 目录 |
test |
运行一次构建,创建一个合并的输出目录,然后运行 dart run test --precompiled <merged-output-dir> |
所有这些命令都支持以下选项:
选项 | 描述 |
--help |
打印帮助信息 |
--delete-conflicting-outputs |
假设用户包中的冲突输出来自之前的构建,并跳过通常提供的用户提示 |
--[no-]fail-on-severe |
是否在记录错误时将构建视为失败。 默认情况下,为 false |
--build-filter |
构建过滤器允许你明确选择要构建的文件,而不是构建整个目录 |
2.2 输入和输出
输入
在Dart中,一个包(package)是包含一组相关Dart代码的目录。有效的输入遵循一般的Dart包规则,一个标准的Dart包通常会有如下的目录结构:
- lib:这个目录包含了包的公共代码。其他包可以导入这个目录下的Dart文件。
- test:这个目录包含了包的测试代码。
- example:这个目录包含了包的示例代码。
- bin:这个目录包含了包的可执行脚本。
有效的输入遵循一般的Dart包规则。
build_runner 可以从任何包依赖项的顶级 lib
文件夹下读取任何文件,也可以从当前包读取所有文件。这个映射定义了构建器(Builder)如何处理输入文件并生成输出文件。所有匹配此映射中任何键的输入源都将作为构建步骤传递给此构建器。只有具有相同基本名和来自此映射值的扩展名的文件才被期望作为输出。
一般来说,最好尽可能具体地指定你的输入集,因为所有匹配的文件都将根据构建器的 buildExtensions 进行检查。
【注】:
buildExtensions
是 Builder类(build库) 的一个属性,它是一个从输入文件扩展名到输出文件扩展名的映射。 例如,如果你有一个将.txt文件转换为.md和.html文件的构建器,那么你的buildExtensions可能如下:
const buildExtensions = { '.txt': ['.md', '.html'], };
这意味着,对于每一个
.txt
文件,这个构建器都会生成一个对应的.md
文件和一个.html
文件。如果存在一个空的键,那么所有的输入都被视为匹配。构建器的实例必须始终返回相同的配置。通常,构建器会返回一个常量映射。构建器也可以根据BuilderOptions选择扩展。
输出
你可以在当前包的任何地方输出文件。构建器不允许覆盖现有文件,只能创建新文件。先前构建的输出不会被视为后续构建的输入。
源代码控制
build_runner 会在你的包的顶级目录下创建一个 .dart_tool
文件夹,这个文件夹不应该提交到你的源代码控制仓库。对于生成的文件,通常最好不要提交它们到源代码控制,但是特定的构建器可能会提供其他的建议。
如果你将生成的文件提交到你的仓库,那么当你切换分支或合并更改时,你可能会在下一次构建时收到关于已存在的声明输出的警告。这将跟随一个提示删除这些文件的提示。你可以输入 l
来列出文件,然后如果一切看起来正确,可以输入 y
来删除它们。如果你认为有什么不对,你可以输入 n
来放弃构建,而不采取任何行动。
3. 应用举例:json_serializable
例如有一个用于生成 JSON 序列化代码的构建器。你可以使用 build_runner 来运行这个构建器,生成序列化代码。
在 Dart 中,我们经常使用 json_serializable 包来自动生成 JSON 序列化和反序列化的代码。这个包提供了一个构建器,我们可以使用 build_runner 来运行这个构建器。
其中:
- json_serializable 提供用于处理 JSON 的Dart 构建系统构建器。
你需要在你的模型类上添加 @JsonSerializable() 注解,并定义 fromJson 和 toJson 方法。然后,json_serializable 构建器会自动为你生成这些方法的实现; - build_runner 用于在 Dart 项目中运行构建器(可以使用 build_runner 来运行任何构建器,包括这里的 json_serializable 提供的构建器,build_runner会找到所有可用的构建器,并按照它们的配置运行它们。)。
也就是说,json_serializable 负责定义如何生成代码,而 build_runner 负责运行 json_serializable 构建器并将生成的代码写入到文件中。
接下来看一下具体的过程。首先安装到依赖:
dependencies: json_annotation: ^4.8.1 dev_dependencies: build_runner: ^2.4.6 json_serializable: ^6.1.4
然后,你可以定义一个 Model 类(数据模型),并使用 json_serializable 的注解:
import 'package:json_annotation/json_annotation.dart'; // 这行是必须的,示user_model.dart文件是一个库的一部分 // 而 user_model.g.dart 文件是这个库的另一部分 part 'user_model.g.dart'; @JsonSerializable() class UserModel { final String name; final String email; UserModel(this.name, this.email); factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json); Map<String, dynamic> toJson() => _$UserModelToJson(this); }
其中,_$UserFromJson 和 _$UserToJson 是由 json_serializable
生成的函数,它们用于将 JSON 数据转换为 UserModel 对象,以及将User对象转换为JSON数据。
接着,在项目下运行 build_runner 的 build 命令来生成序列化代码:
dart run build_runner build
这将会生成一个对应的 user_model.g
文件,这个文件包含了 _$UserFromJson 和 _$UserToJson 的实现。生成的对应的 user_model.g.dart 的代码如下:
// GENERATED CODE - DO NOT MODIFY BY HAND part of 'user_model.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel( json['name'] as String, json['email'] as String, ); Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{ 'name': instance.name, 'email': instance.email, };
4. 应用举例:在 Floor (ORM框架)中自动生成数据库代码
可以参考博文 《用于ORM的Floor框架》,地址:https://jclee95.blog.csdn.net/article/details/133377191
首先,你需要在pubspec.yaml文件中添加floor,floor_generator和build_runner的依赖。floor是运行时依赖,floor_generator和build_runner是开发时依赖。你的pubspec.yaml文件应该像这样:
dependencies: flutter: sdk: flutter floor: ^1.4.2 sqflite: ^2.3.0 dev_dependencies: floor_generator: ^1.4.2 build_runner: ^2.1.2
然后,运行flutter pub get命令来获取这些依赖。
接着创建实体:定义一个类,使用 @entity
注解标记这个类,这个类将会映射到数据库的一个表。使用 @primaryKey
注解标记主键。例如:
// person_entity.dart import 'package:floor/floor.dart'; @entity class PersonEntity { @primaryKey final int id; final String name; PersonEntity(this.id, this.name); }
接下来创建DAO文件,定义一个抽象类,使用@dao注解标记这个类。在这个类中,你可以定义查询数据库的方法。使用@Query注解定义查询语句,使用@insert注解定义插入方法。例如:
// person_dao.dart import 'package:floor/floor.dart'; import 'person_entity.dart'; @dao abstract class PersonDao { @Query('SELECT * FROM Person') Future<List<PersonEntity>> findAllPeople(); @insert Future<void> insertPerson(PersonEntity person); }
接下来,你需要定义一个数据库类,这个类需要继承 FloorDatabase,并使用@Database注解。在这个类中,你可以定义获取PersonDao的方法。例如:
import 'package:floor/floor.dart'; import 'person_dao.dart'; import 'person_entity.dart'; part 'app_database.g.dart'; // 生成的代码会在那里 @Database(version: 1, entities: [PersonEntity]) abstract class AppDatabase extends FloorDatabase { PersonDao get personDao; }
然后,你可以运行命令来生成数据库操作的代码:
dart run build_runner build
可以看到,在同一个目录下生成了一个名为 app_database.g.dart 的文件,生成的内容如下:
// GENERATED CODE - DO NOT MODIFY BY HAND part of 'app_database.dart'; // ************************************************************************** // FloorGenerator // ************************************************************************** // ignore: avoid_classes_with_only_static_members class $FloorAppDatabase { /// Creates a database builder for a persistent database. /// Once a database is built, you should keep a reference to it and re-use it. static _$AppDatabaseBuilder databaseBuilder(String name) => _$AppDatabaseBuilder(name); /// Creates a database builder for an in memory database. /// Information stored in an in memory database disappears when the process is killed. /// Once a database is built, you should keep a reference to it and re-use it. static _$AppDatabaseBuilder inMemoryDatabaseBuilder() => _$AppDatabaseBuilder(null); } class _$AppDatabaseBuilder { _$AppDatabaseBuilder(this.name); final String? name; final List<Migration> _migrations = []; Callback? _callback; /// Adds migrations to the builder. _$AppDatabaseBuilder addMigrations(List<Migration> migrations) { _migrations.addAll(migrations); return this; } /// Adds a database [Callback] to the builder. _$AppDatabaseBuilder addCallback(Callback callback) { _callback = callback; return this; } /// Creates the database and initializes it. Future<AppDatabase> build() async { final path = name != null ? await sqfliteDatabaseFactory.getDatabasePath(name!) : ':memory:'; final database = _$AppDatabase(); database.database = await database.open( path, _migrations, _callback, ); return database; } } class _$AppDatabase extends AppDatabase { _$AppDatabase([StreamController<String>? listener]) { changeListener = listener ?? StreamController<String>.broadcast(); } PersonDao? _personDaoInstance; Future<sqflite.Database> open( String path, List<Migration> migrations, [ Callback? callback, ]) async { final databaseOptions = sqflite.OpenDatabaseOptions( version: 1, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); await callback?.onConfigure?.call(database); }, onOpen: (database) async { await callback?.onOpen?.call(database); }, onUpgrade: (database, startVersion, endVersion) async { await MigrationAdapter.runMigrations( database, startVersion, endVersion, migrations); await callback?.onUpgrade?.call(database, startVersion, endVersion); }, onCreate: (database, version) async { await database.execute( 'CREATE TABLE IF NOT EXISTS `PersonEntity` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY (`id`))'); await callback?.onCreate?.call(database, version); }, ); return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions); } @override PersonDao get personDao { return _personDaoInstance ??= _$PersonDao(database, changeListener); } } class _$PersonDao extends PersonDao { _$PersonDao( this.database, this.changeListener, ) : _queryAdapter = QueryAdapter(database), _personEntityInsertionAdapter = InsertionAdapter( database, 'PersonEntity', (PersonEntity item) => <String, Object?>{'id': item.id, 'name': item.name}); final sqflite.DatabaseExecutor database; final StreamController<String> changeListener; final QueryAdapter _queryAdapter; final InsertionAdapter<PersonEntity> _personEntityInsertionAdapter; @override Future<List<PersonEntity>> findAllPeople() async { return _queryAdapter.queryList('SELECT * FROM Person', mapper: (Map<String, Object?> row) => PersonEntity(row['id'] as int, row['name'] as String)); } @override Future<void> insertPerson(PersonEntity person) async { await _personEntityInsertionAdapter.insert( person, OnConflictStrategy.abort); } }
可以看出,你可能需要手动在app_database.dart导入一些库,一般依据编辑器提示补上就可以:
import 'dart:async'; import 'package:sqflite/sqflite.dart' as sqflite;
在这个生成的文件中,可以看到:
$FloorAppDatabase
:这个类提供了创建AppDatabase实例的方法。你可以使用databaseBuilder方法来创建一个持久化的数据库,或者使用inMemoryDatabaseBuilder方法来创建一个内存数据库。_$AppDatabaseBuilder
:这个类用于构建AppDatabase实例。你可以使用addMigrations方法来添加数据库迁移,使用addCallback方法来添加数据库回调。_$AppDatabase
:这个类是AppDatabase类的实现。它包含了一个PersonDao实例,你可以使用这个实例来操作数据库。_$PersonDao
:这个类是PersonDao接口的实现。它包含了findAllPeople和insertPerson方法的实现,这些方法用于查询和插入数据。
最后,就可以使用生成的 $FloorAppDatabase
类来获取数据库实例,并使用这个实例来操作数据库了。例如:
final database = await $FloorAppDatabase.databaseBuilder('app_database.db').build(); final personDao = database.personDao; final person = PersonEntity(1, 'Frank'); await personDao.insertPerson(person); final result = await personDao.findAllPeople();