引言
最近重构了路由模块,并且学习了一些的flutter路由框架,类似annotation_route、ff_annotation_route、auto_route_library,对于flutter路由有一定的了解,通过这篇文章分享给大家。
环境
windows 10 、Android studio 4.x 、flutter 2.2.3
简介
路由框架的目的:
1、自动化,是将人工操作转化为自动操作,通过程序将路由配置代码自动生成到指定文件,
- 显示转隐式,将页面绑定具体的名称和内联路径名称,方便外部平台调用,并且隐藏具体实现细节。
分析常规用法
以下为路由跳转的逻辑,
Navigator.of(context).push(route);
因为route 对应的对象为页面, 对应抽象类为 PageRoute 类,以下为PageRoute 相关的sdk介绍,默认有三种实现类CupertinoPageRoute,MaterialPageRoute,PageRouteBuilder
PageRoute<T> class Null safety
A modal route that replaces the entire screen.
Inheritance
Object > Route<T> > OverlayRoute<T> > TransitionRoute<T> > ModalRoute<T> > PageRoute
Implementers
CupertinoPageRoute,MaterialPageRoute,PageRouteBuilder
新建一个flutter程序router_demo,测试一下默认的路由跳转写法
@XRouter(
name: "page1",
deeplink: "demo://www.demo.com/page1?title=?&content=?&ext=?")
class Page1 extends StatefulWidget {
Map<String, String> arguments;
Page1(this.arguments);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _Page1();
}
}
@XRouter2(name: "zzz", deeplink: "vvv")
@XRouter(name: "page2", deeplink: "demo://www.demo.com/page2")
class Page2 extends StatefulWidget {
Map<String, String> arguments;
Page2(this.arguments);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _Page2();
}
}
@XRouter(
name: "page3",
deeplink: "demo://www.demo.com/page3?title=?&content=?&ext=?")
class Page3 extends StatefulWidget {
Map<String, String> arguments;
Page3(this.arguments);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _Page3();
}
}
class RouterUtil {
static void pushPage(BuildContext context, Widget widget) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => widget));
}
static void pushName(BuildContext context, String name) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => RouterInfo.getWidgetByName(name)));
}
static void pushDeeplink(BuildContext context, String deeplink) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => RouterInfo.getWidgetByDl(deeplink)));
}
}
static Widget getWidgetByName(String name,
{Map<String, String> arguments = const {}}) {
Widget widget = Container();
switch (name) {
case "page1":
widget = Page1(arguments);
break;
case "page2":
widget = Page2(arguments);
break;
case "page3":
widget = Page3(arguments);
break;
default:
widget = PageNoFound();
break;
}
return widget;
}
static Widget getWidgetByDl(String deeplink) {
Uri uri = Uri.parse(deeplink);
Widget widget = Container();
String dpPreview = getDlPreUri(uri);
switch (dpPreview) {
case "demo://www.demo.com/page1":
widget = Page1(getDlParamUri(uri));
break;
case "demo://www.demo.com/page2":
widget = Page2(getDlParamUri(uri));
break;
case "demo://www.demo.com/page3":
widget = Page3(getDlParamUri(uri));
break;
default:
widget = PageNoFound();
break;
}
return widget;
}
void jump(){
RouterUtil.pushPage(context, Page1({"title":"page1","content":"page1 content"}));
}
void jumpByName(){
RouterUtil.pushName(context, "page2");
}
void jumpByDeeplink(){
RouterUtil.pushDeeplink(context, "demo://www.demo.com/page3?title=page333&content=xxsssd&ext=232323");
// RouterUtil.pushDeeplink(context, "demo://www.demo.com/page3");
}
以上是我们常规添加路由跳转所用的方法.
注解路由框架帮我们自动生成了getWidgetByName 和 getWidgetByDl 中方法体的内容。
我们需要新建一个flutter package程序,或者dart console程序,然后将程序移动到router_demo 的plugins 目录下,修改main.dart
void main(List<String> args) {
print("hello");
}
执行dart run main.dart ,看到terminal面板输出hello
自动生成代码的过程,需要将args 中的参数解析出来,分析原始文件路径,输出文件路径,以及其他信息。然后将带有XRouter 注解的类的类名、注解信息、类构造器信息等,都扫读取出来,组装到数据体中,写入到文件。
1.读取参数
import 'package:router_processor/cmd_model.dart';
CmdModel cmdModel = CmdModel();
void main(List<String> args) {
print("hello");
//
if (args.length == 0) {
return;
}
// parse command
cmdModel = new CmdModel();
cmdModel.classDataModel = new ClassDataModel();
//读取输入输出路径
int index_pi = args.indexOf("-pi");
if (index_pi != -1) {
//存在 -pi 指令
cmdModel.path_in = args[index_pi + 1];
} else {
throw Exception("-pi not null");
}
int index_po = args.indexOf("-po");
if (index_po != -1) {
//存在 -po 指令
cmdModel.path_out = args[index_po + 1];
}
}
print(cmdModel.toString());
执行 dart --no-sound-null-safety run main.dart -pi D:\flutter_router\RouterDemo\lib -po D:\flutter_router\RouterDemo\lib\generated
当然执行指令毕竟不方便,我们可以将指令配置到studio的运行配置项,方便debug调试代码。可参考如下图所示配置:
2.扫描注解类
void scanDartFile(String path) {
Directory lib = new Directory(path);
for (FileSystemEntity item in lib.listSync()) {
final FileStat file = item.statSync();
if (file.type == FileSystemEntityType.file && item.path.endsWith('.dart')) {
scanClassHasAnnotation(item.path);
} else if (file.type == FileSystemEntityType.directory) {
scanDartFile(item.path);
}
}
}
void scanClassHasAnnotation(String item) {
final CompilationUnit astRoot = parseFile(
path: item,
featureSet: FeatureSet.fromEnableFlags(<String>[]), //ClassDeclarationImpl
).unit;
for (CompilationUnitMember unitMember in astRoot.declarations) {
for (final Annotation metadata in unitMember.metadata) {
if (metadata is Annotation &&
metadata.name.name == ("XRouter") &&
metadata.parent is ClassDeclaration) {
cmdModel.routerFileList.add(item);
}
}
}
}
class CmdModel {
String path_in = '';
String path_out = '';
List<String> routerFileList = [];
String appName = '';
ClassDataModel classDataModel = ClassDataModel();
@override
String toString() {
return 'CmdModel{path_in: $path_in, path_out: $path_out, routerFileList: $routerFileList, appName: $appName, classDataModel: $classDataModel}';
}
}
class ClassDataModel {
String importStr = '';
String className = 'RouteInfo';
String caseSb = '';
String caseDlSb = '';
@override
String toString() {
return 'ClassDataModel{importStr: $importStr, className: $className, caseSb: $caseSb, caseDlSb: $caseDlSb}';
}
void appendImport(String import) {
importStr += import;
}
}
3.读取类信息,构造进数据体。
首先我们先复制原来的RouterInfo 的数据体,拆分可变信息到字符串中,
String rootFile = """
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
{0}
class RouterInfo{
static Widget getWidgetByName(String name,
{Map<String, String> arguments = const{}}) {
Widget widget = Container();
switch (name) {
{1}
default:
widget = PageNoFound();
break;
}
return widget;
}
static Widget getWidgetByDl(String deeplink) {
Uri uri = Uri.parse(deeplink);
Widget widget = Container();
String dpPreview = getDlPreUri(uri);
switch (dpPreview) {
{2}
default:
widget = PageNoFound();
break;
}
return widget;
}
static String getDlPre(String deeplink) {
Uri uri = Uri.parse(deeplink);
if (uri.hasQuery) {
String dpPrefix = deeplink.substring(
0, deeplink.length - (uri.query.length + 1));
return dpPrefix;
} else {
return deeplink;
}
}
static String getDlPreUri(Uri uri) {
if (uri.hasQuery) {
String deeplink = uri.toString();
String dpPrefix = deeplink.substring(
0, deeplink.length - (uri.query.length + 1));
return dpPrefix;
} else {
return uri.toString();
}
}
static Map<String, String> getDlParam(String deeplink) {
Uri uri = Uri.parse(deeplink);
if (uri.hasQuery) {
return uri.queryParameters;
} else {
return Map();
}
}
static Map<String, String> getDlParamUri(Uri uri) {
if (uri.hasQuery) {
return uri.queryParameters;
} else {
return Map();
}
}
}
""";
我们缺少的部分是{0}的引用,{1}{2}的case信息。
我们通过观察得知import的结构,类似如下:
import 'package:appName/path/*.dart";
而appName 在yaml文件中,我们引入yaml: ^3.0.0
void parseYaml() {
final String pubspecPath = p.join(
cmdModel.path_in.substring(0, cmdModel.path_in.length - 4),
'pubspec.yaml');
final File pubspec = File(pubspecPath);
if (!pubspec.existsSync()) {
print("not found yaml file");
return;
}
YamlMap yamlMap = loadYaml(pubspec.readAsStringSync());
yamlMap.nodes.forEach((key, value) {
if (key.toString() == "name") {
print("appName:$value");
cmdModel.appName = value.toString();
}
});
}
而path和*.dart ,通过分析路径就可以获取,
void generateRouterClassDataImport() {
for (String item in cmdModel.routerFileList) {
File tmpFile = new File(item);
int lib_index = tmpFile.path.lastIndexOf("\\lib\\");
String relativite_path =
tmpFile.path.substring(lib_index + 5, tmpFile.path.length);
String imp = '';
if (relativite_path.contains("\\")) {
int path_index = relativite_path.lastIndexOf("\\");
imp =
"import 'package:${cmdModel.appName}/${relativite_path.substring(0, path_index)}/${relativite_path.substring(path_index + 1, relativite_path.length)}';\n";
} else {
imp = "import 'package:${cmdModel.appName}/${relativite_path}';\n";
}
cmdModel.classDataModel.appendImport(imp);
}
print("imp----${cmdModel.classDataModel.toString()}");
}
接下来解析注解类的类名、注解信息、构造器信息。
void generateRouterClassDataCase() {
for (String item in cmdModel.routerFileList) {
StringBuffer caseSb = new StringBuffer();
StringBuffer caseDlSb = new StringBuffer();
final CompilationUnit astRoot = parseFile(
path: item,
featureSet: FeatureSet.fromEnableFlags(<String>[]), //ClassDeclarationImpl
).unit;
String curClassName = '';//类名
bool hasParam = false;//构造器是否含参数
for (CompilationUnitMember unitMember in astRoot.declarations) {
for (final Annotation metadata in unitMember.metadata) {
if (metadata is Annotation &&
metadata.name.name == ("XRouter") &&
metadata.parent is ClassDeclaration) {
NodeList<CompilationUnitMember> units = astRoot.declarations;
//解析类信息
for (CompilationUnitMember temp in units) {
if (temp is ClassDeclarationImpl) {
if (temp.extendsClause is ExtendsClauseImpl &&
temp.extendsClause?.superclass.name.name ==
"StatefulWidget") {
curClassName = temp.name.name.toString();
for (SyntacticEntity curEntity
in temp.extendsClause!.parent!.childEntities) {
if (curEntity is ConstructorDeclarationImpl &&
curEntity.parameters is FormalParameterListImpl) {
if (curEntity.parameters.parameters.isNotEmpty) {
hasParam = true;
}
}
}
}
}
}
//解析注解信息
NodeList<Expression>? nodeList = metadata.arguments?.arguments;
for (Expression item in nodeList!) {
if (item is NamedExpressionImpl) {
if (item.name.toString() == "name:") {
String name_expression = item.expression.toSource();
if (name_expression.startsWith("\"")) {
name_expression =
name_expression.substring(1, name_expression.length - 1);
}
if (excludeStr.contains(name_expression)) {
break;
}
caseSb.writeln("case ${item.expression.toSource()}:");
caseSb.writeln(
" widget = ${curClassName}(${hasParam ? "arguments" : ""});");
caseSb.writeln("break;");
}
if (item.name.toString() == "deeplink:") {
String deeplink = item.expression.toSource();
if (deeplink.startsWith("\"")) {
deeplink = deeplink.substring(1, deeplink.length - 1);
}
Uri uri = Uri.parse(deeplink);
String dpPreview = "\"" + RouterInfo.getDlPreUri(uri) + "\"";
caseDlSb.writeln("case ${dpPreview}:");
caseDlSb.writeln(
" widget = ${curClassName}(${hasParam ? "getDlParamUri(uri)" : ""});");
caseDlSb.writeln("break;");
}
}
}
}
cmdModel.classDataModel.caseSb += caseSb.toString();
cmdModel.classDataModel.caseDlSb += caseDlSb.toString();
}
}
}
}
3.构造数据,并写入文件
void generateRouterFile() {
File dstFile;
if (cmdModel.path_out.isEmpty) {
dstFile = new File(cmdModel.path_in + "/" + default_generate_name);
} else {
if (cmdModel.path_out.endsWith(".dart")) {
dstFile = new File(cmdModel.path_out);
} else {
dstFile = new File(cmdModel.path_out + "/" + default_generate_name);
}
}
if (dstFile.existsSync()) {
dstFile.deleteSync();
}
dstFile.createSync();
rootFile = rootFile.replaceAll('{0}', cmdModel.classDataModel.importStr);
rootFile = rootFile.replaceAll('{1}', cmdModel.classDataModel.caseSb);
rootFile = rootFile.replaceAll('{2}', cmdModel.classDataModel.caseDlSb);
dstFile.writeAsStringSync(rootFile);
}
我们执行之后可以看到generated 下生成了我们所需要的文件,但是文件格式太乱了,我们使用dart_style 对dart文件进行格式化,引入dart_style: ^2.0.0
final DartFormatter _formatter = DartFormatter(pageWidth: 100);
Future<void> formatFile(File file) async {
if (file == null) {
return;
}
if (!file.existsSync()) {
print('format error: ${file!.absolute!.path} doesn\'t exist\n');
return;
}
processRunSync(
executable: 'flutter',
arguments: 'format ${file!.absolute?.path}',
runInShell: true,
);
}
void processRunSync({
required String executable,
required String arguments,
bool runInShell = false,
}) {
final ProcessResult result = Process.runSync(
executable,
arguments.split(' '),
runInShell: runInShell,
);
if (result.exitCode != 0) {
throw Exception(result.stderr);
}
print('${result.stdout}');
}
在之前的dstFile.writeAsStringSync(rootFile); 之后执行
formatFile(dstFile);
我们可以看到生成的文件为正常格式。
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:router_demo/module1/page1.dart';
import 'package:router_demo/module2/page2.dart';
import 'package:router_demo/module3/page3.dart';
import 'package:router_demo/nofound/no_found.dart';
import 'package:router_demo/ofound/no_found.dart';
class RouterInfo {
static Widget getWidgetByName(String name,
{Map<String, String> arguments = const {}}) {
Widget widget = Container();
switch (name) {
case "page1":
widget = Page1(arguments);
break;
case "page2":
widget = Page2(arguments);
break;
case "page3":
widget = Page3(arguments);
break;
case "oFound":
widget = PageOFound();
break;
default:
widget = PageNoFound();
break;
}
return widget;
}
static Widget getWidgetByDl(String deeplink) {
Uri uri = Uri.parse(deeplink);
Widget widget = Container();
String dpPreview = getDlPreUri(uri);
switch (dpPreview) {
case "demo://www.demo.com/page1":
widget = Page1(getDlParamUri(uri));
break;
case "demo://www.demo.com/page2":
widget = Page2(getDlParamUri(uri));
break;
case "demo://www.demo.com/page3":
widget = Page3(getDlParamUri(uri));
break;
case "demo://www.demo.com/oFound":
widget = PageOFound();
break;
default:
widget = PageNoFound();
break;
}
return widget;
}
static String getDlPre(String deeplink) {
Uri uri = Uri.parse(deeplink);
if (uri.hasQuery) {
String dpPrefix =
deeplink.substring(0, deeplink.length - (uri.query.length + 1));
return dpPrefix;
} else {
return deeplink;
}
}
static String getDlPreUri(Uri uri) {
if (uri.hasQuery) {
String deeplink = uri.toString();
String dpPrefix =
deeplink.substring(0, deeplink.length - (uri.query.length + 1));
return dpPrefix;
} else {
return uri.toString();
}
}
static Map<String, String> getDlParam(String deeplink) {
Uri uri = Uri.parse(deeplink);
if (uri.hasQuery) {
return uri.queryParameters;
} else {
return Map();
}
}
static Map<String, String> getDlParamUri(Uri uri) {
if (uri.hasQuery) {
return uri.queryParameters;
} else {
return Map();
}
}
}
与之前的文件compare发现一切正常。
这只是一个初稿,实际使用中,可能会有过场动画(CupertinoPageRoute,MaterialPageRoute,PageRouteBuilder
)、状态栏等其他的注解信息,需要大家实际使用过程中自己把握,正所谓 兵无常势水无常形。 适合自己的才是最好!