前言
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 {
@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库完成替换写入