本文教你使用source_gen来开发自己的路由框架了,在开发的时候可以考虑某些功能用此方式开发
source_gen是封装自build和 analyzer,并在此基础上提供友好的api封装。build是一个提供构建控制的库,analyzer是提供dart语法静态分析功能的库,source_gen将其整合便可以实现一套基于注解的代码生成工具。
本文的教程顺序如下:
- 创建项目,添加代码生成库
- 创建注解类
- mustache4dart的使用,创建代码模板
- generator文件的创建,该文件用来生成代码
- builder文件的创建,该文件用来使用generator文件生成指定的dart文件
- build.yaml文件的创建和字段说明
就这么简单,下面我们逐个讲解
1. 创建项目
首先,我们先创建一个Flutter项目,或者一个纯Dart项目,这里我们创建一个纯Dart项目,纯Dart项目是不包含Android和IOS平台代码的,这里我们用不到平台的东西,所以创建纯Dart项目即可
点击Finish,就完成了纯Dart项目的创建(先无视demo,因为我全部创建完以后才截的图)
因为接下来我们要用到source_gen
和mustache4dart
这两个库,所以我们将这两个库加到根目录的pubspec.yaml
里的dependencies
下面,如果只在这个项目里用不提供给其他项目,可以加到dev_dependencies
里
dependencies:
flutter:
sdk: flutter
source_gen:
mustache4dart:
2. 创建注解类
创建文件core.dart
, 我们把我们所需要的注解类和辅助类都放到这里,方便管理。那么都需要什么注解呢,如果跳转,我们需要知道要跳转的页面的路径、可能携带的参数、跳转成功的widget。
注意: 注解必须有const的构造函数
完整代码core.dart
/// 作者:liuhc
/// 定义页面路由注解
/// Define page routing annotations
class EasyRoutePathAnnotation {
final String url;
final bool hasParam;
const EasyRoutePathAnnotation(this.url, this.hasParam);
}
/// EasyRoutePathAnnotation的hasParam为true的时候必须添加一个接受此参数的构造函数
/// When the hasParam of EasyRoutePathAnnotation is true, you must add a constructor that accepts this parameter.
class EasyRouteParam {
final Map<String, dynamic> params;
EasyRouteParam(this.params);
}
/// 定义路由注解
/// Define route parser annotations
class EasyRouterAnnotation {
const EasyRouterAnnotation();
}
然后这步就结束了。EasyRoutePathAnnotation
是我们要添加到被跳转页面的注解。EasyRouteParam
这个类需要所有通过我们的路由器跳转的页面添加带EasyRouteParam
参数的构造器。EasyRouterAnnotation
用来注解我们封装的路由器类,这个路由器类调用我们通过source_gen
生成的类,路由器类不是必须的,但是我们必须随便注释一个类来生成实际的路由跳转代码。
3. mustache4dart的使用,创建代码模板
到这里我们就用到mustache4dart这个类库了,我们这个框架的这一步是最重要的,用这个库,我们可以很方便的生成代码,不熟悉的可以看官方文档(文章底部提供了地址),我们这里用到的它的api并不多,我们的模板代码如下,这里我给这个文件起名叫generate_code_template.dart
/// author:liuhc
const String codeTemplate = """
import 'package:easy_router/easy_router.dart';
import 'package:flutter/widgets.dart';
{{#imports}}
import '{{{path}}}';
{{/imports}}
class EasyRouter {
static EasyRouter get instance => _getInstance();
static EasyRouter _instance;
EasyRouter._internal();
factory EasyRouter()=> _getInstance();
static EasyRouter _getInstance() {
if (_instance == null) {
_instance = EasyRouter._internal();
}
return _instance;
}
final Map<String, Pair<dynamic, bool>> _routeMap = {{{routeMap}}};
Widget getWidget(String url, {Map<String, dynamic> param}) {
try {
final Type pageClass = _routeMap[url].clazz;
if (pageClass == null) {
return null;
}
final bool hasParam = _routeMap[url].hasParam;
return _createInstance(pageClass, hasParam, param);
} catch (e) {
print(e.toString());
return null;
}
}
dynamic _createInstance(Type clazz, bool hasParam, Map<String, dynamic> param) {
{{{classInstance}}}
}
}
class Pair<E, F> {
E clazz;
F hasParam;
Pair(this.clazz, this.hasParam);
}
""";
这个文件里的imports
、routeMap
、routeMap
一会都会被替换成实际代码,编写这个文件的时候,除了占位代码,其他可以像平时一样来写,最后在最前面和最后面加上"""即可。
4. generator文件的创建,该文件用来生成代码
首先我们需要根据注解,将url和对应的Widget保存到一个map集合里,还需要一个集合用来保存import的内容,然后再将这些变量替换代码模板里的占位符,然后生成代码即可,我们分以下几步
1 将url和对应的Widget保存到名为routeMap的集合里
2 将Widget所在文件保存到名为imports的集合里
3 将routeMap和imports替换代码模板里的占位符
第1、2步骤我写到了generator_param.dart
文件里,在这一步,我们并不需要生成任何文件,这一步只需要将第3步需要的参数生成即可。
/// author:liuhc
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'core.dart';
/// This file is used to save the parameters needed to generate the code.
/// 该文件用来保存生成代码所需的参数
class EasyRoutePathGenerator extends GeneratorForAnnotation<EasyRoutePathAnnotation> {
static Map<String, Pair<dynamic, bool>> routeMap = {};
static List<String> importList = [];
static String classInstanceContent;
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
routeMap = _parseRouteMap(routeMap, element, annotation);
importList = _parseImportList(importList, buildStep);
classInstanceContent = _generateInstance(routeMap);
return null;
}
/// 1 Save the url and the corresponding Widget to a collection named routeMap
/// 1 将url和对应的Widget保存到名为routeMap的集合里
Map<String, dynamic> _parseRouteMap(
Map<String, Pair<dynamic, bool>> routeMap, Element element, ConstantReader annotation) {
String clazz = element.displayName;
print("parse element=$clazz");
String url = annotation.peek('url').stringValue;
bool hasParam = annotation.peek('hasParam').boolValue;
/// May be a bug in mustache4dart, if you don't add ' before and after the url, the generated code is problematic.
/// 可能是mustache4dart的bug,如果不给url前后添加'的话,生成的代码是有问题的
String urlKey = "'" + url + "'";
if (routeMap.containsKey(urlKey)) {
return routeMap;
}
routeMap[urlKey] = Pair(clazz, hasParam);
return routeMap;
}
/// 2 Save the file where the Widget is located to a collection named imports
/// 2 将Widget所在文件保存到名为imports的集合里
List<String> _parseImportList(List<String> importList, BuildStep buildStep) {
String path = buildStep.inputId.path;
print("parse path=$path");
if (path.contains("lib/")) {
path = path.replaceFirst("lib/", "");
}
if (!importList.contains(path)) {
importList.add(path);
}
return importList;
}
/// Generate a switch statement to get different Widgets through different urls
/// 生成switch语句,通过不同的url获取不同的Widget
String _generateInstance(Map<String, Pair<dynamic, bool>> routeMap) {
StringBuffer stringBuffer = StringBuffer();
stringBuffer.writeln("switch (clazz) {");
routeMap.forEach((String url, Pair<dynamic, bool> pair) {
if (pair.hasParam) {
stringBuffer.writeln("case ${pair.clazz} : ");
stringBuffer.writeln("EasyRouteParam easyRouteParam = EasyRouteParam(param);");
stringBuffer.writeln("return ${pair.clazz}(easyRouteParam);");
} else {
stringBuffer.writeln("case ${pair.clazz} : return ${pair.clazz}();");
}
});
stringBuffer.writeln("default: return null;}");
return stringBuffer.toString();
}
}
class Pair<E, F> {
E clazz;
F hasParam;
Pair(this.clazz, this.hasParam);
}
然后第3步,生成代码,这里我们给该文件取名为generator_router.dart
/// author:liuhc
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:mustache4dart/mustache4dart.dart';
import 'package:source_gen/source_gen.dart';
import 'core.dart';
import 'generate_code_template.dart';
import 'generator_param.dart';
/// This file is used to generate EasyRouter
/// 该文件用来生成EasyRouter
class EasyRouterGenerator extends GeneratorForAnnotation<EasyRouterAnnotation> {
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
/// 3 Replace placeholders with routemap and imports in the code template
/// 3 将routeMap和imports替换代码模板里的占位符
return render(codeTemplate, <String, dynamic>{
'imports': EasyRoutePathGenerator.importList.map((item) => {'path': item}).toList(),
'classInstance': EasyRoutePathGenerator.classInstanceContent,
'routeMap': EasyRoutePathGenerator.routeMap
.map((String key, Pair<dynamic, bool> value) => MapEntry(key, "Pair(${value.clazz},${value.hasParam})"))
.toString()
});
}
}
5. builder文件的创建,该文件用来使用generator文件生成指定的dart文件
在我们运行命令生成代码的时候,我们会指定builder文件的位置,然后脚本会自动根据该文件来生成代码,文件名字随意,这里我们就叫builder.dart
,代码如下
完整代码builder.dart
/// author:liuhc
import 'package:build/build.dart';
import 'package:easy_router/src/generator_router.dart';
import 'package:source_gen/source_gen.dart';
import 'generator_param.dart';
/// Does not generate files here
/// 这里并不生成文件
Builder paramBuilder(BuilderOptions options) =>
LibraryBuilder(EasyRoutePathGenerator(), generatedExtension: ".empty.dart");
/// 生成".g.dart"结尾的文件
/// Generate a file ending with ".g.dart"
Builder routerBuilder(BuilderOptions options) =>
LibraryBuilder(EasyRouterGenerator(), generatedExtension: ".g.dart");
6. build.yaml文件的创建和字段说明
在项目的根目录下创建build.yaml
文件,文件名字必须是这个,文件内容如下
完整代码build.yaml
# Read about `build.yaml` at https://pub.flutter-io.cn/packages/build_config
# import指定了builder的位置,
# builder_factories指定了builder的具体调用,
# build_extensions指定了输入输入文件的格式匹配,
builders:
param_builder:
import: 'package:easy_router/src/builder.dart'
builder_factories: ['paramBuilder']
build_extensions: { '.dart': ['.g.dart'] }
auto_apply: root_package
build_to: source
router_builder:
import: 'package:easy_router/src/builder.dart'
builder_factories: ['routerBuilder']
build_extensions: { '.dart': ['.g.dart'] }
auto_apply: root_package
build_to: source
路由器框架完成
到这里我们的路由框架就完成了
编写测试demo
我们写个demo来测试一下,在当前项目里根目录运行命令flutter create demo
,这个demo项目会包含android和ios平台。
注意,这里用命令行来创建demo,如果使用as创建的话,as不会把demo创建到当前项目里
因为我们需要使用刚才写好的路由库,还需要source_gen
生成代码,所以修改demo下面的pubspec.yaml
文件,在dev_dependencies
下面添加如下代码
dev_dependencies:
build_runner:
easy_router:
path: ../
至于
dependencies
和dev_dependencies
的区别不是本文的重点,欲知详情自己谷歌
然后我们在我们的demo下面添加测试代码
main.dart
/// description: easy_router demo app
/// author: liuhc
import 'package:flutter/material.dart';
import 'router.dart';
void main() {
runApp(
MaterialApp(
title: '简单路由',
home: MainPage(),
theme: ThemeData(
primarySwatch: Colors.blue,
),
),
);
}
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("首页"),
),
body: ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_getButton(context, "pageA", "跳转到页面A", param: {"key": "a"}),
_getButton(context, "pageB", "跳转到页面B"),
_getButton(context, "pageC", "跳转到不存在的页面"),
],
),
),
);
}
Widget _getButton(
BuildContext context,
String url,
String text, {
Map<String, dynamic> param,
}) {
return RaisedButton(
onPressed: () {
Router.instance.go(context, url, param: param);
},
child: Text(text),
);
}
}
page_a.dart
/// description: test page A
/// author: liuhc
import 'package:flutter/material.dart';
import 'package:easy_router/easy_router.dart';
@EasyRoutePathAnnotation("pageA", true)
class PageA extends StatelessWidget {
final EasyRouteParam param;
PageA(this.param);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Page A"),
),
body: Container(
alignment: Alignment.center,
child: Text("param:${param["key"]}"),
),
);
}
}
page_b.dart
/// description: test page B
/// author: liuhc
import 'package:flutter/material.dart';
import 'package:easy_router/easy_router.dart';
@EasyRoutePathAnnotation("pageB", false)
class PageB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Page B"),
),
body: Container(
alignment: Alignment.center,
child: Text("no param"),
),
);
}
}
router.dart
/// 作者:liuhc
import 'package:easy_router/easy_router.dart' show EasyRouterAnnotation;
import 'package:flutter/material.dart';
@EasyRouterAnnotation()
class Router {
static Router get instance => _getInstance();
static Router _instance;
Router._internal();
factory Router() => _getInstance();
static Router _getInstance() {
if (_instance == null) {
_instance = Router._internal();
}
return _instance;
}
Widget getWidget(String url, {Map<String, dynamic> param}) {
//TODO
}
void go(BuildContext context, String url, {Map<String, dynamic> param}) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return getWidget(url, param: param);
},
),
);
}
}
class NotFoundPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("404"),
),
body: Container(
alignment: Alignment.center,
child: Text("没有找到页面"),
),
);
}
}
运行命令生成代码
然后在demo目录下运行命令生成代码
flutter packages pub run build_runner build --delete-conflicting-outputs
推荐生成代码之前先清除以前的代码
flutter packages pub run build_runner clean
然后就生成了router.g.dart
文件
然后我们修改刚才的router.dart
文件,添加import 'router.g.dart';
,修改getWidget
方法
Widget getWidget(String url, {Map<String, dynamic> param}) {
return EasyRouter.instance.getWidget(url, param: param) ?? NotFoundPage();
}
完成demo
到这里,我们的demo就完成了,运行一下,就看到效果了
如何在项目中使用
该项目开发中需要注意的地方:
我们的core.dart
文件里不能import 'package:flutter/widgets.dart'
,否则生成代码的时候会报错
参考文章: