從頭編寫一個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閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容