Flutter系列之路由框架

1,595 阅读8分钟

为什么要自己写路由框架?

开发中最常用到的就是路由了,很多入坑flutter的小伙伴们也必然百度,google了一大堆的路由框架,五花八门,功能也是千奇百怪,我也是看了好多的路由框架。

路由框架那么多,到底哪一款适合自己。

我们基本都会遇到这样几个问题:

  1. 我就用个路由功能,但是它却自带了太多的其他功能,用不到,不用又浪费。
  2. 需要某个功能的时候,框架还不支持,可能已经有计划支持,但实际时间不确定,除了fork人家代码,改人家代码,还能有什么办法呢。
  3. 使用的时候遇到了一个问题,提了issue,但人不理你或者几个月后理你了,emmmm。

所以,最好的办法是什么,自己写一个。

所以,一个Idea,一个框架就这样诞生了。

思考路由框架的结构应该是什么样的?

很多时候,想要学习一个三方的优秀框架,如果一味地去看源码,可能会一团乱麻,并不是学习能力不够,是学习的方式不对。我们首先应该去思考,了解整个框架的基本构成,结构,然后再针对性地去了解,去学习。那么,写一个框架的前提,肯定也是构思框架应该具有什么,框架内部应该是一个什么样的流程。

当然,写之前也不需要考虑得太全面,那样会导致你不知道从何下手。

而一个最简易的路由框架,应该需要包含以下三个部分。后面就来手动实操一下最简易的一个路由框架吧。

路由框架基础结构.png

实现一个最基础的路由框架

初始化项目

flutter create --template=package --project-name=router_anno

  1. 执行上述命令生成一个package项目,并在yaml中引用source_gen;如下:
dependencies:
  source_gen: 1.0.3
  build: 2.0.3
  analyzer: 1.7.1
  1. 在lib同级目录创建一个build.yaml文件,并写入如下配置:
targets:
  $default:
    builders:
      tdf_router_anno|tdf_router_builder:
        enabled: true

builders:
  tdf_router_builder:
    import: 'package:tdf_router_anno/builder.dart' #1
    builder_factories: ['tdfRouterBuilder'] #2
    build_extensions: { '.dart': ['.tdf_router.dart'] } #3 路由文件后缀
    auto_apply: dependents
    build_to: source
    applies_builders: ["source_gen|combining_builder"]

具体的参数意义和用法,官方可以直接找到,我就不详细赘述了。 3. 看到第二步中的#1处,找到这个builder.dart,在其内配置注解的生成器:

Builder tdfRouterBuilder(BuilderOptions options) =>
    LibraryBuilder(TZRouterGenerator(), generatedExtension: '.tdf_router.dart');

到这里,已经将注解相关的配置都完成了,后面就是在生成器TZRouterGenerator中编码了。

定义、注册注解

在flutter中,定义一个注解十分简单,定义一个普通的类,其就能作为注解被使用。

// TZRouter注解
class TZRouter {
  final String path; // 路由path
  const TZRouter(
      {required this.path});
}

上面的注解中,只定义了一个最基础的路由注解的path参数,先以最基本的构成来实现。其他的参数会在文末拓展中进行添加。

注解定义完成后,怎么跟生成器绑定呢?

class TZRouterGenerator extends GeneratorForAnnotation<TZRouter> {
  @override
  generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
      return "";
  }

上述代码中,可以看出,GeneratorForAnnotation内的泛型参数,传入了TZRouter,也就是说注解生成器TZRouterGenerator只会解析TZRouter这个注解的数据。(绑定)

确定想要生成什么样的代码

思考原生在跳转页面的时候,需要的是什么信息。

  1. 路由path
  2. path对应的Widget
getRouterPath() {
  return "test/test1";
}

Widget getPageWidget(params) {
  return MyApp(params);
}

我们生成以上两个方法即可。

到了这一步,我们已经拥有了所有的路由信息,下一步就是将其注册到flutter主工程中。

路由注册

含义:

将path和对应Widget,注册到路由管理map中。

这里有两种实现思路,

  1. 一种仍旧是采用注解的形式,定义一个新的注解,该注解给flutter主工程的main方法上。之后在注解解析时,去分析flutter整个项目的所有#3处作为后缀的文件,读取其路径并import,获取其内path和widget,注册到统合map中。

(这里就不好意思了,思路是这样的,但是我没写,我觉得太麻烦了,且通过注解的形式要分析整个项目,时间上比较长,flutter注解分析代码确实慢,跟原生要经过编译一个意思,且生成代码的方式编写较复杂。)

  1. 通过python,分析flutter主模块内依赖的所有模块,并找到其引用的位置,进入lib源代码目录,查找到所有#3处后缀的文件。后续就是跟注解的形式一模一样了。这种方式分析模块依赖很简单且不需要经过编译这一步,从命令开始执行到代码生成,一共就零点几秒。

大致讲一下怎么分析吧。

2.*版本的flutter,可以通过解析.packages文件,拿到所有依赖库的lib目录所在,再执行文件夹遍历即可。

3.*版本的flutter,可以通过分析.dart_tool/package_config.json文件,python分析json文件更是手到擒来了。

具体的实现方式这边就不写啦,没啥好多说的,主要还是思路。

拓展(框架迭代)

公司的路由框架还是经过了好几次的迭代了,有新的需求,就有新的迭代嘛。

迭代1.自动生成路由文档

第一次迭代,给了路由文档。那一天,领导说,flutter业务迭代比较快,但是路由管理这方面,还没有做起来,需要有一个完善的文档知道现在有哪些页面,具体在哪个模块内。

image.png

这次的迭代给路由注解增加了一个note必填参数,其中填写的是该路由页面的描述。路由注解在生成路由描述文件时,多生成一个方法如下:

getNote() {
  return "页面描述文案";
}

再然后就通过python分析并生成代码后,再生成一份相应的路由表即可。

路由文档作用

  • 能查看当前flutter具体的使用情况,
  • 能快速查找定位页面所在位置,所在模块。
  • 快速查看是否开发过类似页面或者相应业务的页面,这对于前期不熟悉业务的时候,很有帮助。

迭代2.统一完整的flutter页面入口

这一次迭代,是为了有一个快速入口,能够直接打开指定页面,如果原生已经有一个快速口子,那么可以将flutter的页面也接入到其内。

image.png

本次迭代对路由注解新增了一个isMainPage参数,代表该页面是否是某一块业务的首页。这里涉及到一个问题,如果某一个页面它需要接收一些参数才能正常展示,那么它就不需要配置到快速入口那边,因为快速入口一个是给开发使用,一个是给产品使用,产品肯定是不知道应该传递什么参数的。

实现方式还是采用python分析后,自动再生成一个debug的入口页面,并将配置了isMainPage为true的页面入坑按钮自动生成到入口页面内。

路由描述文件也相应增加一个方法:

bool isMainPage() {
  return false;
}

迭代3.适配Flutter2.2.3 -> 3.3.10

本次迭代没什么好说的,就是分析文件由原来的.packages改为了.dart_tool/package_config.json

迭代4.注解参数增加params

为什么会有这次迭代呢

  • 原因1

我们写代码时,需要写很多模版代码。举个例子,当Widget加了注解之后,原先采用的是单Map<String, dynamic类型的参数,作为Widget的唯一参数传入Widget,构造Widget。如下:

class MyApp extends StatelessWidget {
  final Map<String, dynamic> params;
  MyApp(this.params);
  @override
  Widget build(BuildContext context) {
  }
}

如果想要在params内取一个key为name的String数据,那么就需要编写如下这样的模版代码:

late String name;
if (params.containsKey('name') && params['name'] is String) {
    name = params['name'];
} else {
    name = '';
}

或者使用 ?? 操作符去简化代码,但是遇到null情况或者外部传入的类型不是预期类型时,总是难以避免写一些上述的if模版代码

  • 原因2 当页面入参复杂时,由于各人习惯不同,从params中获取参数的时机也各不相同,导致其他人使用已存在的页面时,无法第一时间知道需要传入哪些参数。可能造成参数漏传或错传。所以在注解中增加一个params参数,用法如下:
@TZRouter(
    path: 'test/test1',
    params: {
      "name": "xj",
      "age": 18,
      "male": true,
    },
    defaultParameterFieldName: "params", # 这个可以不传,默认为"params"
    note: "")
class MyApp extends StatelessWidget {
  final Map<String, dynamic> params;
  MyApp(this.params);
  @override
  Widget build(BuildContext context) {
  }
}

加上注解后,执行注解生成代码命令,会生成如下拓展:

extension MyAppRouterParamsExtension on MyApp {
  String get name {
    if (params.containsKey('name') && params['name'] is String) {
      return params['name'];
    }
    return 'xj';
  }
  
  int get age {
    if (params.containsKey('age') && params['age'] is int) {
      return params['age'];
    }
    return 1;
  }

  bool get male {
    if (params.containsKey('male') && params['male'] is bool) {
      return params['male'];
    }
    return true;
  }
}

上述拓展拓展了MyApp这个页面类。当MyApp文件内import该拓展时:

  • 可直接在MyApp中直接使用变量name,age,male,且已为其赋值,不再需要考虑空指针问题。
  • 减少了手动编写模版代码的时间。
  • 规范了页面获参的方式。

至此,路由注解已经变成了这样:

// TZRouter注解
class TZRouter {
  final String path; // 页面path
  final String note; // 页面描述
  final Map<String, dynamic>? params; // 页面参数
  final String defaultParameterFieldName; // 默认参数名
  final bool isMainPage; // 是否是业务首页
  const TZRouter({
    required this.path,
    required this.note,
    this.isMainPage = false,
    this.params,
    this.defaultParameterFieldName = 'params',
  });
}

最后还是提一句吧。

如果觉得三方框架不能很好地满足自身需求,那么,动起手来,写一个吧。

如果觉得自己没有的功能,别人的框架里有,又比较眼红,那么,拿来吧你。