Flutter系列:4.基于注解的代码生成应用
前言
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库完成替换写入