APT 是 Annotation Processing Tool 的缩写,即注解处理器。熟悉 Java/Android 开发的小伙伴应该对 APT 并不陌生,例如在 Android 生态中,基本上所有的路由框架都会使用到 APT,通过给 Activity 标记带有路由信息的注解,在编译期通过 APT 扫描被注解的类,获取页面和路由的映射关系,再通过代码生成框架(如 javapoet)生成中间类,然后在 transform 阶段汇总路由表。。。
而在 Dart 中,可以借助官方提供的 source_gen 来实现注解处理和代码的生成。下面我们以一个例子来介绍 soruce_gen 的使用。
创建工程
首先确保电脑安装了 dart 环境,vscode,dart 的 vscode 插件。
打开 vscode,打开命令面板(macOS 快捷键是 ⇧⌘P 或者 F1),搜索 dart 关键字,选择 「Dart: New Project」,选择 「Console Application」,这样就生成了一个默认的 console application 工程。
使用 source_gen 需要在 pubspec.yaml 中添加依赖:
1 2 3
| dependencies: source_gen: build_runner:
|
我们还是以路由为例,期望给 page 添加带路由名称的注解,在页面跳转的时候,传入路由名称即可。
假设存在页面 HomePage:
1
| class HomePage extends Page {}
|
正常的页面跳转:
1
| Navigator.push(HomePage());
|
期望的页面跳转:
1
| Navigator.push("/home");
|
创建注解
dart 的注解不像 java 或者 kotlin 那样,会有特定的标识(@interface / annotation class),dart 的注解只需要将类的构造函数定义成 const 即可。
下面我们创建一个 Route 注解:
1 2 3 4 5 6
| class Route { final String name; const Route({required this.name}); }
|
并在 HomePage 类上添加注解:
1 2
| @Route(name: "/home") class HomePage {}
|
生成代码
既然我们希望使用路由名称代替创建页面实例,那就需要生成一个路由和实例的映射关系,我们可以先写好我们期望生成的代码模版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import 'page.dart';
typedef PageCreator = Page Function();
class RouteCollector { RouteCollector._internal(); static final RouteCollector _instance = RouteCollector._internal(); factory RouteCollector() => _instance;
PageCreator? getPageCreator(String route) => _routeTable[route];
static final Map<String, PageCreator> _routeTable = { "/home": () => HomePage(), "/main": () => MainPage(), }; }
|
我们可以想一想,仅通过一步能否生成上面的代码?
其实是不行的,由于是根据注解来生成的代码,在扫描的时候生成的代码是一对一的,即每一个被注解的类对应一段生成的代码,仅通过一步仅仅能生成类似 "/home": () => HomePage(),
这样的代码,无法把每个注解生成的代码聚合成上面我们期望生成的那样。所以需要两步,第二步把第一步生成的中间代码聚合起来,形成最终的我们期望的代码。(当然,也可以像 Android 那样,每个注解生成一个完整的类,第二步聚合这些类。)
创建 Generator
source_gen 包中提供了 GeneratorForAnnotation
,我们需要创建一个 Generator 并继承它,就可以在 generateForAnnotatedElement 方法中获取到注解的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import 'package:build/src/builder/build_step.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:dart_source_gen/route.dart'; import 'package:source_gen/source_gen.dart';
class RouteMetaGenerator extends GeneratorForAnnotation<Route> { @override generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep, ) { final route = annotation.peek('name')?.stringValue; final page = element.name; return '"$route": () => $page(),'; } }
|
第一步生成类似如下的代码:
1
| "/home": () => HomePage(),
|
创建 Builder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart';
import 'generator.dart';
Builder routeBuilder(BuilderOptions options) => LibraryBuilder( RouteMetaGenerator(), generatedExtension: '.route', formatOutput: (code) => code, header: '', allowSyntaxErrors: true, );
|
然后在工程的根目录下创建 build.yaml 文件,并配置 builder:
1 2 3 4 5 6 7
| builders: routeBuilder: import: 'package:dart_source_gen/builder.dart' builder_factories: ['routeBuilder'] build_extensions: { ".dart": [ ".route" ] } auto_apply: root_package build_to: cache
|
build_to 有 source 和 cache 两种模式,source 会生成文件,cache 则在内存中,这里使用 cache 是因为中间生成的代码只是给第二步使用的临时代码。
然后运行:
1
| flutter packages pub run build_runner build --delete-conflicting-outputs
|
至此,第一步的中间代码已经生成了,如果想看输出的效果,可以把 build_to 改成 source 尝试:
1 2 3 4 5 6 7 8 9 10 11
| // ************************************************************************** // RouteMetaGenerator // **************************************************************************
import "page1.dart"; "/home": () => HomePage(),
import "page1.dart"; "/main": () => MainPage(),
|
第二步,需要我们自己实现一个 Builder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| import 'dart:async'; import 'dart:convert';
import 'package:build/build.dart'; import 'package:glob/glob.dart'; import 'package:source_gen/source_gen.dart'; import 'package:collection/collection.dart'; import 'package:dart_style/dart_style.dart';
Builder routeCollectBuilder(BuilderOptions options) => RouteCollectBuilder();
class RouteCollectBuilder implements Builder { @override FutureOr<void> build(BuildStep buildStep) async { final inputIds = await buildStep.findAssets(Glob('**/*.route')).toList(); final imports = <String>{}; final contents = []; for (final inputId in inputIds) { final content = await buildStep.readAsString(inputId); List<String> lines = const LineSplitter().convert(content); lines.removeWhere((line) => line.isEmpty || line.startsWith("//")); final groupedLines = lines.groupListsBy((line) { return line.startsWith('import') ? 0 : 1; }); final import = groupedLines[0]; if (import != null && import.isNotEmpty) { imports.addAll(import); } final code = groupedLines[1]; if (code != null && code.isNotEmpty) { contents.add(code.join('\n')); } }
final code = """ import 'page.dart'; ${imports.join('\n')}
class RouteCollector { RouteCollector._internal(); static final RouteCollector _instance = RouteCollector._internal(); factory RouteCollector() => _instance;
PageCreator? getPageCreator(String route) => _routeTable[route];
static final Map<String, PageCreator> _routeTable = { ${contents.join('\n')} }; } """;
buildStep.writeAsString( AssetId(buildStep.inputId.package, 'bin/route_table.dart'), DartFormatter().format(code), ); }
@override Map<String, List<String>> get buildExtensions => const { r'lib/$lib$': ['bin/route_table.dart'] }; }
|
同样,需要在 build.yaml 中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| builders: routeBuilder: import: 'package:dart_source_gen/builder.dart' builder_factories: ['routeBuilder'] build_extensions: { ".dart": [ ".route" ] } auto_apply: root_package build_to: source collectPageMetadataBuilder: import: 'package:dart_source_gen/builder.dart' builder_factories: [ "routeCollectBuilder" ] build_extensions: { ".dart": [ "bin/route_table.dart" ] } auto_apply: root_package required_inputs: ['.route'] build_to: source
|
再次运行 flutter packages pub run build_runner build --delete-conflicting-outputs
,生成如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import 'page.dart'; import "page1.dart"; import "detail/datail.dart"; import "page2.dart";
class RouteCollector { RouteCollector._internal(); static final RouteCollector _instance = RouteCollector._internal(); factory RouteCollector() => _instance;
PageCreator? getPageCreator(String route) => _routeTable[route];
static final Map<String, PageCreator> _routeTable = { "/home": () => HomePage(), "/main": () => MainPage(), "/detail": () => DetailPage(), "/page2": () => Page2(), }; }
|