前言
api 数据序列化为 model 实例是移动开发中很常见也是很基础的技术点,得益于运行时等动态技术在 ios 开发中我们可以借助 JSONModel 或者 SwiftyJSON 很方便的实现序列化,对于刚刚接触 flutter 的开发者来说其序列化体验无疑是非常糟糕的。本身 Dart 语言是支持反射的,但是在 Flutter 中,Dart 几乎放弃了脚本语言动态化的特性,如不支持反射、也不支持动态创建函数等;所以序列化只有依靠拦截注解来动态生成代码的方式实现。
注解
注解是一种可以为代码提供一些语义信息或元数据的标注,这在其他语言中也很常见,在 dart 中常见的注解有 @deprecated、@override 等,注解是以 @开头的,他们可以作用于类,函数,属性等。
dart 中自定义注解很简单,其实现就是一个带有 const 构造函数的类
library todo;
class Todo {
final String who;
final String what;
const Todo(this.who, this.what);
}
然后就可以这样使用 Todo 这个注解了
import ‘todo.dart’;
@Todo(‘seth’, ‘make this do something’)
void doSomething() {
print(‘do something’);
}
source_gen
通过注解的方式我们就可以为类或者属性添加一个额外的数据信息,source_gen 可以拦截注解获取并解析上下文信息,通过解析注解实现 source_gen 的相关 Generator 就可以动态的生成代码了;
source_gen 是封装自 build 和 analyzer,并在此基础上提供友好的 api 封装。build 是一个提供构建控制的库,analyzer 是提供 dart 语法静态分析功能的库,source_gen 将其整合便可以实现一套基于注解的代码生成工具。
代码生成
使用 Annotation+source_gen 的方式可以便捷的生成代码,source_gen 通过拦截 Annotation,解析其上下文 element 然后通过 builder 即可动态生成代码,下面简易的代码生成 Demo。
创建 package
终端运行:
flutter create –template=package code_gen_demo
vscode 打开刚刚创建的 package, pubspec.yaml 添加 source_gen 和 build_runner 依赖
dependencies:
flutter:
sdk: flutter
source_gen: ‘>=0.8.0’
lib 目录下创建注解 mark.dart
class Mark {
final String name;
const Mark({this.name});
}
创建代码生成器 generator.dart 负责拦截我们的注解 Mark, 解析注解的类名称,路径及其参数 name 并返回
import ‘package:analyzer/dart/element/element.dart’;
import ‘package:source_gen/source_gen.dart’;
import ‘package:build/build.dart’;
import ‘mark.dart’;
class MarkGenerator extends GeneratorForAnnotation<Mark> {
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
String className = element.displayName;
String path = buildStep.inputId.path;
String name =annotation.peek(‘name’).stringValue;
return “//$className\n//$path\n//$name”;
}
}
lib 目录创建构建器 builder.dart, 添加一个顶级方法 markBuilder 供 build runner 解析调用
import ‘package:source_gen/source_gen.dart’;
import ‘package:build/build.dart’;
import ‘mark_generator.dart’;
Builder markBuilder(BuilderOptions options) => LibraryBuilder(MarkGenerator(),
generatedExtension: ‘.mark.dart’);
在 package 根目录下添加 build.yaml 文件 (buildRunner 会解析其配置执行 builder 指定的方法),配置成刚刚创建的 builder 内容如下
targets:
$default:
builders:
code_gen_demo|mark_builder:
enabled: true
builders:
mark_builder:
import: ‘package:code_gen_demo/builder.dart’
builder_factories: [‘markBuilder’]
build_extensions: {‘.dart’: [‘.mark.dart’] }
auto_apply: root_package
build_to: source
import 指定了 builder 的位置,builder_factories 指定了 builder 的具体调用,build_extensions 指定了输入输入文件的格式匹配,此列会生成 ”.mark.dart” 结尾的文件。
至此代码生成相关的 Annotation、builder 和 Generator 都准备好了,接下来我们创建 example 工程来做示例
创建 example 工程
在 package 的根目录下创建 example 工程,example 是一个完整的 flutter 工程,执行命令:
flutter create example
在 example 工程中引入我们的 package, 在 example 的 pubspec.yaml 中添加依赖 package,以及添加对 builder_runner 的依赖来执行编译命令
dependencies:
flutter:
sdk: flutter
code_gen_demo:
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ‘>=0.9.1’
创建一个示例类,mark_demo.dart, 并添加 Mark 注解
import ‘package:code_gen_demo/mark.dart’;
@Mark(name: “hello”)
class MarkDemo {
}
好了,接下来在 example 目录下执行 builder runner 命令来为 Mark 注解的 mark_demo.dart 生成一个相关代码 mark_demo.mark.dart
flutter packages pub run build_runner build –delete-conflicting-outputs
重新执行 run builder_runner 前最好先 clean 一下
flutter packages pub run build_runner clean
命令执行完成后就可以看到在 mark_demo.dart 文件下生成了一个 mark_demo.mark.dart 的文件,其内容是 mark_generator.dart 中为 Mark 这个注解创建的 Generator 返回的内容:
// GENERATED CODE – DO NOT MODIFY BY HAND
// **************************************************************************
// MarkGenerator
// **************************************************************************
//MarkDemo
//lib/mark_demo.dart
//hello
本 demo 源码位置 GitHub
easy_router
目前在 Flutter 中常见的代码生成主要应用在 json 序列化库 json_serializable 中,在国内闲鱼技术团队使用这一技术实现了一套 router 的路由映射解决方案 annotation_route, 感兴趣的可以看看。
作为学习我参考了闲鱼的 annotation_route 实现了一个简单的 Flutter 页面路由匹配方案 easy_router,不同于闲鱼 annotation_route 的复杂和全面,简单实现路由 url 的匹配、参数解析赋值并返回 page 实例。
easy_router 源码戳我
使用方式
使用 @EasyRoute 来注解需要加入 Router 的 page, url 作为 page 的唯一标识,例如
@EasyRoute(url: “easy://flutter/pagea”)
class PageA extends StatefulWidget {
final EasyRouteOption routeOption;
PageA(this.routeOption);
@override
_PageAState createState() => _PageAState();
}
easy_router 会调用 page 的构造函数并传入 EasyRouteOption 参数,所以每个 page 都应该有一个这样的构造函数,如果 url 有参数,参数会放到 EasyRouteOption 对象的 params 属性中,以便 page 获取。
使用 @easyRouter 来注解你的 router, 这样就会生成 router 相关的内部逻辑, 例如
import ‘package:example/route.router.internal.dart’;
import ‘package:easy_router/route.dart’;
@easyRouter
class Router {
EasyRouterInternal internalImpl = EasyRouterInternalImpl();
dynamic getPage(String url) {
EasyRouteResult result = internalImpl.router(url);
if(result.state == EasyRouterResultState.NOT_FOUND) {
print(“Router error: page not found”);
return null;
}
return result.widget;
}
}
EasyRouterInternalImpl 就是最终生成的 router 实现, 执行命令生成 EasyRouterInternalImpl 实现
flutter packages pub run build_runner build –delete-conflicting-outputs
调用 router 打开 url 对应的 page
MaterialButton(
child: Text(‘ToPageA’),
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return Router().getPage(‘easy://flutter/pagea?parama=a’);
}
)
);
},
),
感兴趣自己改改,详细使用参看源码 example
实现方式
routeParseBuilder:负责解析 @EasyRoute 注解的 page 页面,完成 page 和 url 的映射关系 routerBuilder:读取 routeParseBuilder 生成的映射,完成对 EasyRouterInternalImpl 写入,依赖 mustache4dart 库完成替换写入