Dart Apt 使用 source_gen 做 flutter 页面跳转 page router

code_gen

a apt tools provider some annotation to generate dart code ,like router make page jump simple

example

flutter中无法使用反射做hook,通常使用Apt比较多,基于builder_runner(Dart代码生成文件库)的 source_gen 可实现注解生成代码 类似 java 的
AbstractProcessor

目标:自动生成路由配置,页面带参跳转,参数获取

page_router项目地址:

使用方法:

# pubspec.yaml 引入
dependencies:
  #  flutter 项目下 dependencies 引入 code_gen (使用 source_gen 编写的 aop lib)
  code_gen:
    git:
      url: git://github.com/iloveq/code_gen.git
      ref: main

dev_dependencies:
  #  flutter 项目下 dev_dependencies 引入 builder_runner
  build_runner: ^1.10.0

1:创建 app_router_table.dart 工厂模式引用生成的类(实现了模版方法configureRoutes),通过 build_runner 会生成文件 app_router_table.table.dart

import 'package:code_gen/router_gen.dart';
import 'package:flutter/cupertino.dart';
import 'app_router_table.table.dart'; // 生成的文件

@RouterTable()
abstract class AppRouterTable {

  factory AppRouterTable() = $AppRouterTable; // 生成的类

  Map<String, WidgetBuilder> configureRoutes();

}

2:MyApp 配置路由表

Widget _buildMaterialApp() {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        platform: TargetPlatform.iOS,
        primarySwatch: Colors.blue,
      ),
      routes: AppRouterTable().configureRoutes(),
      initialRoute: "/",
    );
  }

3:注解标记需要注册的Page和参数Arg

import '../config/app_router_table.table.dart'; // 引入生成文件
// 主页
@RouterPage(isIndex: true)
class HomePage extends StatefulWidget {
  HomePage({Key key}) : super(key: key);
@override
  _HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        child: Text("home page"),
        // 跳转(调用生成方法)
        onTap: ()=> context.navigator2TestPage(url: "from home page")
      ),
    );
  }
}
...
// testPage
import '../config/app_router_table.table.dart';
@RouterPage() // 标明页面 Page
class TestPage extends StatefulWidget{
  @RouterArg(required: true) // 标明参数 Arg
  final String url = "";

  TestPage({Key key}) : super(key: key);

  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage>{
  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        child: Text(context.getTestPageArguments().url), // 获取参数
        onTap: ()=> context.navigator2MinePage(num:2000),
      ),
    );
  }
}
...

4:使用 flutter packages pub run build_runner build 生成文件 app_router_table.table.dart:

export 'app_router_table.table.dart';
import 'app_router_table.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_app/pages/test_page.dart';
import 'package:flutter_app/pages/home_page.dart';
import 'package:flutter_app/pages/mine_page.dart';

class $AppRouterTable implements AppRouterTable {
  @override
  Map<String, WidgetBuilder> configureRoutes() {
    return <String, WidgetBuilder>{
      '/': (context) => HomePage(),
      'TestPage': (context) => TestPage(),
      'Mine': (context) => MinePage(),
    };
  }
}

// **************************************************************************
// TestPage

class TestPageArguments {
  final String url;

  TestPageArguments({@required this.url});
}

extension TestPageContext on BuildContext {
  void navigator2TestPage({@required String url}) {
    Navigator.pushNamed(this, "TestPage",
        arguments: TestPageArguments(url: url));
  }

  TestPageArguments getTestPageArguments() {
    return ModalRoute.of(this).settings.arguments;
  }
}

// **************************************************************************

// **************************************************************************
// MinePage

class MinePageArguments {
  final int num;

  MinePageArguments({this.num});
}

extension MinePageContext on BuildContext {
  void navigator2MinePage({int num}) {
    Navigator.pushNamed(this, "Mine", arguments: MinePageArguments(num: num));
  }

  MinePageArguments getMinePageArguments() {
    return ModalRoute.of(this).settings.arguments;
  }
}

// **************************************************************************

那么如何创建一个Aop 功能的 lib ,以下是通过 source_gen 做的一个Aop 工具 :

我们先来看 page_router 的包结构:

├── README.md
├── build.yaml
├── lib
│   ├── builder.dart
│   ├── router_gen.dart
│   └── src
│       ├── annotation
│       │   ├── router_arg.dart
│       │   ├── router_page.dart
│       │   └── router_table.dart
│       ├── generator
│       │   ├── router_generator.dart
│       │   └── router_table_generator.dart
│       └── tools
│           └── router_collector.dart
├── pubspec.lock
└── pubspec.yaml

1: code_gen 文件夹下,创建 pubspec.yaml 文件 引入 builder_runner 和 source_gen 及 dart 配置,并运行 pub get

name: code_gen
description: auto generate router params
version: 0.0.1
author: haoran
homepage: 1549112908@qq.com
environment:
  sdk: ">=2.1.0 <3.0.0"
dependencies:
  analyzer: any
  build: any
  build_config: '>=0.3.0'
  source_gen: ^0.9.7
dev_dependencies:
  build_runner: ^1.10.0

2: 创建 build.yaml 配置注解生成器信息

targets:
  $default:
    builders:
       # code_gen 工程下的 router_gen_builder(builder 名字随意,和下面对应就可以)
      code_gen|router_gen_builder:
        options: { 'write': true }
        enabled: true
        generate_for:
          exclude: ['**.params.g.dart']
      code_gen|router_table_gen_builder:
        options: { 'write': true }
        enabled: true
        generate_for:
          exclude: ['**.table.dart']


builders:
  router_gen_builder:
    import: "package:code_gen/builder.dart" # builder.dart 文件位置
    builder_factories: ["generateRouterParams"] # 对应 build.dart 文件中的方法
    build_extensions: {".dart": ['.params.g.dart']} # 生成文件后缀名
    auto_apply: dependents
    build_to: source
    # runs_before 先于 router_table_gen_builder 执行
    runs_before: ['code_gen|router_table_gen_builder'] 
  router_table_gen_builder:
    import: "package:code_gen/builder.dart"
    builder_factories: ["generateRouterTable"]
    build_extensions: {".dart": ['.table.dart']}
    auto_apply: dependents
    build_to: source

3:看到上面创建了 build.dart 文件,这个是类似于 java Aop 的 resource/META-INF.services 配置 Processor,相当于生成器的入口

// build.dart
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'src/generator/router_generator.dart';
import 'src/generator/router_table_generator.dart';

Builder generateRouterParams(BuilderOptions options) =>
    LibraryBuilder(RouterGenerator(), generatedExtension:'.params.g.dart');

Builder generateRouterTable(BuilderOptions options)=>
    LibraryBuilder(RouterTableGenerator(), generatedExtension: '.table.dart');

4: 定义注解,创建注解生成器

// 定义注解:
class RouterTable{
  const RouterTable();
}
class RouterPage {
  final bool isIndex;
  final String path;
  const RouterPage({this.path = "",this.isIndex = false});
}
class RouterArg {
  final bool required;
  const RouterArg({this.required = false});
}
// 创建注解生成器:
// 1 RouterGenerator:
class RouterGenerator extends GeneratorForAnnotation<RouterPage> {
  static RouterCollector collector = RouterCollector();

  @override
  generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    print(element);
    if (element.kind == ElementKind.CLASS) {
      var importStr = "";
      if (buildStep.inputId.path.contains('lib/')) {
        importStr =
            "package:${buildStep.inputId.package}/${buildStep.inputId.path.replaceFirst('lib/', '')}";
      } else {
        importStr = "${buildStep.inputId.path}";
      }
      collector.importList.add(importStr);
      String className = element.name;
      String aptRouterPath = annotation.read("path").stringValue;
      String routerName = aptRouterPath != null && aptRouterPath.isNotEmpty
          ? aptRouterPath
          : className;
      var page = Page();
      page.arguments = [];
      for (FieldElement e in ((element as ClassElement).fields)) {
        List<ElementAnnotation> fieldAnnotationList = e.metadata;
        fieldAnnotationList.forEach((element) {
          if (element.toString().startsWith("@RouterArg")) {
            Argument argument = Argument();
            argument.isRequired = element
                .computeConstantValue()
                .getField("required")
                .toBoolValue();
            argument.name = e.name;
            print("arguments-field: ${e.toString()}");
            String type_ = e.toString().split(" ")[0];
            argument.type = type_.replaceAll("*", "");
            page.arguments.add(argument);
            print("arguments: ${argument.toString()}");
          }
        });
      }
      if (aptRouterPath == "/" || annotation.read("isIndex").boolValue) {
        page.routerPath = "/";
        page.name = className;
        collector.indexRouter["/"] = page;
      } else {
        page.name = className;
        page.routerPath = routerName;
        collector.routerMap[routerName] = page;
      }
    }
    return null;
  }
}
// 2 RouterTableGenerator:
class RouterTableGenerator extends GeneratorForAnnotation<RouterTable> {
  @override
  generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    if (element.kind == ElementKind.CLASS) {
      String path = buildStep.inputId.path; // lib/xxx.dart
      String relatedFileName = Path.basename(path); // xxx.dart
      String relatedClassName = element.name;
      return generateRouterTable(relatedFileName, relatedClassName);
    }
    return "class TestTable{}";
  }

  String generateRouterTable(String relatedFileName, String relatedClassName) {
    String export = relatedFileName.split(".")[0] +
        ".table." +
        relatedClassName.split(".")[1];
    String imports = "";
    String routerMap = "";
    var ArgsAndNavigatorExtension = "";
    for (String import in RouterGenerator.collector.importList) {
      imports = imports + "import '" + import + "';\n";
    }
    RouterGenerator.collector.routerMap.forEach((key, value) {
      routerMap = routerMap + "'${key}':( context ) => ${value.name}(),";
      ArgsAndNavigatorExtension =
          ArgsAndNavigatorExtension + _genArgsAndNavigatorExtension(value);
    });

    return """
export '${export}';
import '${relatedFileName}';
import 'package:flutter/cupertino.dart';
${imports}

class \$${relatedClassName} implements ${relatedClassName}{    

  @override
  Map<String, WidgetBuilder> configureRoutes() {
    return <String,WidgetBuilder>{
      '/': ( context) => ${RouterGenerator.collector.indexRouter['/'].name}(),
      ${routerMap}
    };
  }  
}

${ArgsAndNavigatorExtension}

""";
  }
}

String _genArgsAndNavigatorExtension(Page page) {
  var fields = "";
  var argument = "";
  var extension = "";
  var constructorParams = ""; // this.a,this.b
  var functionParams = ""; // String a,String b
  var selectedConstructorParams = ""; // a:a, b:b
  if (page.arguments != null && page.arguments.isNotEmpty) {
    var size = page.arguments.length;
    for (int i = 0; i <= size - 1; i++) {
      fields = fields +
          "final " +
          page.arguments[i].type +
          " " +
          page.arguments[i].name +
          ";\n";
      constructorParams = constructorParams +
          (page.arguments[i].isRequired ? "@required " : "") +
          "this." +
          page.arguments[i].name +
          (size == 1 || i == size - 1 ? "" : ",");
      functionParams = functionParams +
          (page.arguments[i].isRequired ? "@required " : "") +
          page.arguments[i].type +
          " " +
          page.arguments[i].name +
          (size == 1 || i == size - 1 ? "" : ",");
      selectedConstructorParams = selectedConstructorParams +
          page.arguments[i].name +
          " : " +
          page.arguments[i].name +
          (size == 1 || i == size - 1 ? "" : ",");
    }
  }
  var explainName = "${page.name}";
  argument = _genArgument(page, fields, constructorParams);
  extension =
      _genNavigatorExtension(page, functionParams, selectedConstructorParams);
  return """
// **************************************************************************   
// ${explainName}

$argument

$extension

// **************************************************************************  

""";
}

String _genNavigatorExtension(
    Page page, String functionParams, String selectedConstructorParams) {
  if (page.arguments == null || page.arguments.isEmpty) {
    return """
extension ${page.name}Context on BuildContext{
  void navigator2${page.name}(){
    Navigator.pushNamed(this, "${page.routerPath}");
  }
}    
""";
  } else {
    return """
extension ${page.name}Context on BuildContext{
  void navigator2${page.name}(
      {${functionParams}}){
    Navigator.pushNamed(this, "${page.routerPath}",
        arguments:${page.name}Arguments(${selectedConstructorParams})
    );
  }
  ${page.name}Arguments get${page.name}Arguments(){
    return ModalRoute.of(this).settings.arguments;
  }
}    
""";
  }
}

String _genArgument(Page page, String fields, String constructorParams) {
  if (page.arguments == null || page.arguments.isEmpty) {
    return "";
  }
  return """
class ${page.name}Arguments{
    ${fields}
    ${page.name}Arguments({${constructorParams}}); 
}  
""";
}

tools : 定义一些数据结构 Page/Arg ,存储 router_gen_builder 解析带 @RouterPage 标记带类信息的结果,用于之后执行 router_table_gen_builder 解析 @RouterTable 生成 .table.dart 类

class RouterCollector<T> {
  List<String> importList = <String>[];
  Map<String, Page> routerMap = <String, Page>{};
  Map<String, Page> indexRouter = <String, Page>{};
}

class Page {
  String routerPath;
  String name;
  List<Argument> arguments;
  @override
  String toString() {
    return "{ routerPath:${this.routerPath},name:${this.name},arguments:${this.arguments.toString()}}";
  }
}

class Argument {
  String name;
  String type;
  bool isRequired;

  @override
  String toString() {
    return "{ name:${this.name},type:${this.type},isRequired:${this.isRequired}}";
  }
}

感谢:)

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

推荐阅读更多精彩内容