美文网首页
从头编写一个flutter 注解路由框架

从头编写一个flutter 注解路由框架

作者: 匠人plus | 来源:发表于2022-07-05 00:13 被阅读0次

引言

最近重构了路由模块,并且学习了一些的flutter路由框架,类似annotation_routeff_annotation_routeauto_route_library,对于flutter路由有一定的了解,通过这篇文章分享给大家。

环境

windows 10 、Android studio 4.x 、flutter 2.2.3

简介

路由框架的目的:
1、自动化,是将人工操作转化为自动操作,通过程序将路由配置代码自动生成到指定文件,

  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


微信图片_20220704224828.png

当然执行指令毕竟不方便,我们可以将指令配置到studio的运行配置项,方便debug调试代码。可参考如下图所示配置:


微信图片_20220704225129.png

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发现一切正常。


微信图片_20220704231203.png

demo地址

这只是一个初稿,实际使用中,可能会有过场动画(CupertinoPageRoute,MaterialPageRoute,PageRouteBuilder
)、状态栏等其他的注解信息,需要大家实际使用过程中自己把握,正所谓 兵无常势水无常形。 适合自己的才是最好!

相关文章

网友评论

      本文标题:从头编写一个flutter 注解路由框架

      本文链接:https://www.haomeiwen.com/subject/xdmfbrtx.html