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

引言

最近重构了路由模块,并且学习了一些的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
)、状态栏等其他的注解信息,需要大家实际使用过程中自己把握,正所谓 兵无常势水无常形。 适合自己的才是最好!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容