转载请注明出处

Flutter-通过注解自动生成路由

在使用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, "哎嘿嘿");
}));

这样做的弊端有很多:

  1. 每个路由都需要单独手动添加,每次维护映射管理时都需要补全 Switch 分支和定义路由名。
  2. 传参的时候不够明显,没有语法提示,在调用传参的时候,不能很明确的知道对方需要什么参数。

通过注解实现

页面

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
#pubspec.yaml
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,});
}

/// 定义router文件注解类
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());
}
}
}
""";
}

/// 返回路由 case 结构
String Function(Map<String, String>, Map<String, List<List<String>>>) _genRouteCases = (Map<String, String> animationList, Map<String, List<List<String>>> params) {
String _case = "";

// 把key按顺序排序,以固定代码格式不会乱,避免git的代码冲突严重
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;
};

/// 对应的 Params
/// 如果没有指定参数类型,则默认为 String
/// {
/// "page1": ["int:a", "bool:b", "double:c", "string:e", "a"],
/// "page2": ["int:a", "bool:b", "double:c", "string:e", "a"]
/// }
String Function(Map<String, List<List<String>>>) _genParamClass = (Map<String, List<List<String>>> params) {
String _class = "";
// 把key按顺序排序,以固定代码格式不会乱,避免git的代码冲突严重
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);
}

// 将route加入到map里
// className 一定要唯一啊
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) {
// 进行排序,由于解析顺序每次都一定是一样的
// 为了保持代码顺序一致,避免在 git 仓库里每次变动太大,导致冲突
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 注解处理及代码生成

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×