在使用Flutter
开发业务时由于业务比较复杂,涉及到的页面太多,到目前为止已经有100+
页面。所以需要一个轻量级的路由框架来管理路由。本文基于dart
注解机制实现。 在开始前 Google 了很多相关的解决方案,但是由于项目已经开发到后期页面过多,使用其他框架侵入式太严重需要修改大量代码以适配,遂放弃使用现有的框架。
原来的路由结构 页面 1 2 3 4 5 6 7 8 9 10 class Examples extends StatelessWidget { final id; final name; Examples(this .id, this .name}); @override Widget build(BuildContext context) { return Text("$this .id, $this .name" ); } }
路由表(手动维护) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class RouteName { static const String NotFount = 'notFount' ; static const String Examples = 'examples' ; } class Router { static Route<dynamic > generateRoute(RouteSettings settings) { switch (settings.name) { case RouteName.Examples: return Examples(); default : return NotFount(); } }
调用方法 1 2 3 4 5 Navigator.pushNamed(context, RouteName.Examples); Navigator.of(context).push(new MaterialPageRoute(builder: (_) { return Examples(1 , "哎嘿嘿" ); }));
这样做的弊端有很多:
每个路由都需要单独手动添加,每次维护映射管理时都需要补全 Switch 分支和定义路由名。
传参的时候不够明显,没有语法提示,在调用
和传参
的时候,不能很明确的知道对方需要什么参数。
通过注解实现 页面 1 2 3 4 5 6 7 8 9 10 11 12 @HtPage ( params: ['int:id' , 'String:name' ] ) class Examples extends StatelessWidget { ExamplesParams params; Examples({this .params}); @override Widget build(BuildContext context) { return Text("$this .params.id, $this .params.name" ); } }
路由表(自动生成) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class RouteName { static const String NotFount = 'notFount' ; static const String Examples = 'examples' ; } class ExamplesParams { final int id; final String name; ExamplesParams({this .id, this .name}); } class Router { static Route<dynamic > generateRoute(RouteSettings settings) { switch (settings.name) { case RouteName.Examples: return Examples(params: settings.arguments); default : return NotFount(); } }
调用方法 1 2 3 4 Navigator.pushNamed(context, RouteName.Examples, arguments: ExamplesParams( id: 1 , name: "哎嘿嘿" ));
注解的实现 注解,实际上是代码级的一段配置,它可以作用于编译时或是运行时,由于目前flutter不支持运行时的反射功能,我们需要在编译期就能获取到注解的相关信息,通过这些信息来生成一个自动维护的映射表。那我们要做的,就是在编译时通过分析dart文件的语法结构,找到文件内的注解块和注解的相关内容,对注解内容进行收集,最后生成我们想要的映射表.
新建一个Flutter包
首先先建立个Flutter
的包
1 flutter create --template=package ht_router
创建成功之后,需要在项目里面加入两个包文件,注解依赖的。
1 2 3 4 5 6 7 8 9 10 11 12 dependencies: flutter: sdk: flutter source_gen: dev_dependencies: flutter_test: sdk: flutter build_runner:
建立注解类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class HtRouterAnimation { static const String PageRouteNoAnimBuilder = "PageRouteNoAnimBuilder" ; static const String PageRouteFadeBuilder = "PageRouteFadeBuilder" ; static const String PageRouteSlideDirection = "PageRouteSlideBuilder" ; } class HtPage { final String animation; final List <String > params; const HtPage({this .params, this .animation = HtRouterAnimation.PageRouteSlideDirection,}); } class HtRouter { const HtRouter(); }
很简单的几行代码,这步就完成了。由于dart
的注解机制是,一个注解类包只能生成一个文件且一个注解类只能处理一种注解标识,这里使用的是 HtRouter
来生成文件,前面的 HtPage
是用来处理并缓存没个注解的(这后面会有代码展示)。
代码模板库用于生成最终的代码字符串(generate_code_template.dart) 一会都会被替换成实际代码,实际上就和平时写代码差不多,只不过变成生成字符串了。
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 class GenerateCodeTemplate { final List <String > routeNames; final Map <String , List <List <String >>> params; final List <String > importList; final Map <String , String > animationList; final String packageName; GenerateCodeTemplate({this .packageName,this .animationList,this .routeNames, this .params, this .importList}); String code () { String routeNames = _genRouteNames(this .routeNames); String paramsClass = _genParamClass(this .params); String routeCases = _genRouteCases(this .animationList, params); String imports = _genImports(this .importList); return """ import 'package:flutter/cupertino.dart'; import 'package:$packageName /widget/page_route_anim.dart'; import 'package:$packageName /pages/404/index.dart'; $imports class RouteName { $routeNames } $paramsClass class Router { static Route<dynamic> generateRoute(RouteSettings settings) { switch (settings.name) { $routeCases default: return PageRouteFadeBuilder(NotFount()); } } } """ ; } String Function (Map <String , String >, Map <String , List <List <String >>>) _genRouteCases = (Map <String , String > animationList, Map <String , List <List <String >>> params) { String _case = "" ; List <String > keys = animationList.keys.toList(); keys.sort(($1 , $2 ) => $1. compareTo($2 )); keys.forEach((key) { String value = animationList[key]; String paramName = '' ; if (params.containsKey(key)) { paramName = 'params: settings.arguments' ; } _case += """ case RouteName.$key : return $value ($key ($paramName )); """ ; }); return _case; }; String Function (List <String >) _genImports = (List <String > importList) { String _imports = "" ; importList.forEach((v) { _imports += "$v ;\n" ; }); return _imports; }; String Function (List <String >) _genRouteNames = (List <String > routeNames) { String _field = "" ; routeNames.forEach((v) { _field += "static const String $v = '$v ';\n" ; }); return _field; }; String Function (Map <String , List <List <String >>>) _genParamClass = (Map <String , List <List <String >>> params) { String _class = "" ; List <String > keys = params.keys.toList(); keys.sort((String $1 , String $2 ) => $1. compareTo($2 )); keys.forEach((String key) { List <List <String >> arr = params[key]; final String className = key + "Params" ; String _constructor = "" ; String _field = "" ; arr.forEach((v) { _field += "final ${v[1 ]} ${v[0 ]} ;\n" ; _constructor += "this.${v[0 ]} ," ; }); if (_constructor != "" ) { _constructor = "{$_constructor }" ; } _class += """ class $className { $_field $className ($_constructor ); } """ ; }); return _class; }; }
生成最终代码及数据处理(generator.dart) 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 63 64 65 66 67 68 69 70 71 72 73 import 'package:analyzer/dart/element/element.dart' ;import 'package:build/build.dart' ;import 'package:ht_router/generate_code_template.dart' ;import 'package:source_gen/source_gen.dart' ;import './core.dart' ;class HtPageGenerator extends GeneratorForAnnotation <HtPage > { static List <String > routeList = []; static List <String > importList = []; static Map <String , List <List <String >>> paramsList = {}; static Map <String , String > animationList = {}; @override generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { final List params = annotation.peek('params' )?.listValue; final String animation = annotation.peek('animation' ).stringValue; final String className = element.displayName; String path = "import '${buildStep.inputId.uri.toString()} '" ; if (importList.contains(path) == false ) { importList.add(path); } if (routeList.indexOf(className) == -1 ) { routeList.add(className); animationList[className] = animation; if (params != null && params.length > 0 ) { paramsList[className] = []; params?.forEach((v) { List <String > split = v.toString().split(":" ); String type = "" ; String name = "" ; if (split.length > 2 ) { throw Exception("解析参数错误:" + v); } if (split.length == 1 ) { type = "String" ; name = v; } type = split[0 ].replaceFirst("String ('" , "" ).replaceFirst("')" , "" ); name = split[1 ].replaceFirst("String ('" , "" ).replaceFirst("')" , "" ); paramsList[className].add([name, type]); }); } } return null ; } } class HtRouteGenerator extends GeneratorForAnnotation <HtRouter > { @override generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { HtPageGenerator.routeList.sort((String $1 , String $2 ) => $1. compareTo($2 )); HtPageGenerator.importList.sort((String $1 , String $2 ) => $1. compareTo($2 )); return GenerateCodeTemplate( routeNames: HtPageGenerator.routeList, params: HtPageGenerator.paramsList, importList: HtPageGenerator.importList, animationList: HtPageGenerator.animationList, packageName: buildStep.inputId.package, ).code(); }}
在上面的 generateForAnnotatedElement
中的 element
有如下属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 element.toString: class Examples element.name: Examples element.kind: CLASS element.displayName: Examples element.documentationComment: null element.enclosingElement: ht_router|lib/core.dart element.hasAlwaysThrows: false element.hasDeprecated: false element.hasFactory: false element.hasIsTest: false element.hasLiteral: false element.hasOverride: false element.hasProtected: false element.hasRequired: false element.isPrivate: false element.isPublic: true element.isSynthetic: false element.nameLength: 9 element.runtimeType: ClassElementImpl
如果是关于构建的环境信息则在 buildStep
里。
建立 builder.dart 文件 在我们执行命令生成程式码的时候,我们会指定builder文件的位置,然后指令会自动根据该文件来生成代码,代码如下:
1 2 3 4 5 6 7 8 9 10 import 'generator.dart'; import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart'; Builder pageBuilder (BuilderOptions options) => LibraryBuilder(HtPageGenerator(), generatedExtension: ".tt.dart"); Builder routerBuilder (BuilderOptions options) => LibraryBuilder(HtRouteGenerator());
build.yaml 的建立 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 targets: $default: builders: ht_router|page_builder: enabled: true ht_router|router_builder: enabled: true builders: page_builder: import: 'package:ht_router/builder.dart' builder_factories: ['pageBuilder'] build_extensions: { '.dart' : ['.tt.dart'] } auto_apply: root_package build_to: cache router_builder: import: 'package:ht_router/builder.dart' builder_factories: ['routerBuilder'] build_extensions: { '.dart' : ['.g.dart'] } auto_apply: root_package build_to: source
到此路由框架代码就全部完毕
使用 在我们项目的 pubspec.yaml
文件中,添加我们写的路由框架。 由于路由框架只在开发的时候使用,所以添加到 dev_dependencies
字段里。
1 2 3 4 dev_dependencies: build_runner: ht_router: path: 对应的路径
具体使用方法可以见文章开头。 给页面写好注解后,通过命令行生成最终文件。
1 flutter packages pub run build_runner build --delete-conflicting-outputs
如果生成的的文件和结果不一致,建议先清除构建文件
1 flutter packages pub run build_runner clean
上面的代码和我们的项目有深度依赖,并不一定适配其他项目,仅做记录作用
参考文章:掘金:Flutter 注解处理及代码生成