1、项目背景

几乎所有的Flutter应用都是采用路由表的方式对路由进行管理,即在应用初始化时,提前把路由名称和对应的页面注册到路由表中,应用内部通过路由名称跳转到相应的页面。以如下Demo项目为例:

1.1、项目目录结构

image.png

整个项目包含三个页面(大厅页面、设置页面A、设置页面B) 以及 一个入口文件(main.dart)

1.2、项目代码说明

A、B 设置页面(屏幕正中间展示当前页面名称)

class APage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('APage'),
    );
  }
}


class BPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('BPage'),
    );
  }
}

大厅页面(屏幕正中间展示页面名称,点击名称跳转到页面A)

class HomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: () => Navigator.pushNamed(context, '/setting_a'),  //路由跳转
        child: Text('HomePage'),
      ),
    );
  }
}

入口文件(注册应用路由表)

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      initialRoute: '/home',
      routes: {
        '/home': (context) => HomePage(),    //路由注册
        '/setting_a': (context) => APage(),
        '/setting_b': (context) => BPage(),
      },
    );
  }
}

2、项目问题

随着项目的不断开发迭代,会有越来越多的页面被添加到应用中。由于新增加的页面都需要提前注册到路由表中,此时入口文件(main.dart)会变得越来越臃肿:

routes: {
        '/home': (context) => HomePage(),
        '/setting_a': (context) => APage(),
        '/setting_b': (context) => BPage(),
        ...
        ...
},

不容小觑的还有另外一个问题,项目正在变得越来越扁平化!!! ,毕竟项目路由并没有分结构进行管理:

image.png

3、解决方案

3.1、路由注册方案改造

image.png

在项目的入口文件中,只需要添加对应的业务模块,而页面的注册过程就交给对应的模块完成,保证项目结构化的同时,也极大避免了入口文件臃肿问题。

3.2、模块管理基类创建

class MixinRouterContainer {
  ///init router
  Map<String, WidgetBuilder> installRouters() => {};

  ///open page
  Future<T?>? openPage<T>(BuildContext context, String pageName, ... Map<dynamic, dynamic>? arguments,...}) {
    Map<String, dynamic> args = {'args': arguments};
    switch (pushType) {
      case RoutePushType.pushNamed:
        return Navigator.pushNamed(context, pageName, arguments: args);
      ...
    }
  }
}

整个基类的核心包括两个方法:

  • installRouters:  配置属于该模块的路由表
  • openPage: 打开相应的路由页面

3.3、模块页面注册:

mixin HomeRouteContainer on MixinRouterContainer {
  @override
  Map<String, WidgetBuilder> installRouters() {
    Map<String, WidgetBuilder> originRoutes = super.installRouters();
    Map<String, WidgetBuilder> newRoutes = {};
    newRoutes['/home'] = (context) => HomePage(); //注册大厅页面
    newRoutes.addAll(originRoutes);
    return newRoutes;
  }
}

mixin SettingRouteContainer on MixinRouterContainer {
  @override
  Map<String, WidgetBuilder> installRouters() {
    Map<String, WidgetBuilder> originRoutes = super.installRouters();
    Map<String, WidgetBuilder> newRoutes = {};
    newRoutes['/setting_a'] = (context) => APage();  //注册A页面  
    newRoutes['/setting_b'] = (context) => BPage();  //注册B页面
    newRoutes.addAll(originRoutes);
    return newRoutes;
  }
}

可以看到HomeRouteContainer 把 HomePage添加到自己的路由表中,同样SettingRouteContainer管理了SettingA、SettingB两个页面。

3.4、App模块注册:

class AppRouteContainer extends MixinRouterContainer
    with HomeRouteContainer, SettingRouteContainer {  //通过mixin机制粘合项目各个路由模块
  AppRouteContainer._();

  static AppRouteContainer _instance = AppRouteContainer._();

  static AppRouteContainer get share => _instance;
}


class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      initialRoute: '/home',
      routes: AppRouteContainer.share.installRouters(),  //注册总路由表
    );
  }
}

要注意的是,需要创建一个新的类,来粘合项目所有的路由模块,如上面的AppRouteContainer所示,声明成一个单例,方便在项目中使用:

  • 注册项目路由表:AppRouteContainer.share.installRouters()
  • 页面跳转:AppRouteContainer.share.openPage(context, '/setting_a')

4、方案延伸

情况一:

说明: 在项目开发过程中,除了简单的页面跳转外,还存在路由拦截。比如:用户在没登录的情况下,想打开个人主页,那么就需要拦截这一过程,并跳转到登录页面。

解决方案:只需在原有的路由管理模块的基类( MixinRouterContainer )上,做进一步的封装。通过添加拦截路由表,并重写路由跳转过程:

typedef MixinRouteInterceptor = bool Function(BuildContext context, String pageName, ...);

class MixinRouterInterceptContainer extends MixinRouterContainer {
  
  final Map<String, MixinRouteInterceptor> _routeInterceptorTable = {};

  void registerRouteInterceptor(String pageName, MixinRouteInterceptor interceptor) {
    _routeInterceptorTable[pageName] = interceptor;
  }

  void unRegisterRouteInterceptor(String pageName) {
    _routeInterceptorTable.remove(pageName);
  }

  @override
  Future<T?>? openPage<T>(BuildContext context, String pageName,...) {
    if (!_routeInterceptorTable.containsKey(pageName)) {
      return super.openPage(context,pageName,...);
    }
    MixinRouteInterceptor interceptor = _routeInterceptorTable[pageName]!;
    bool needIntercept = interceptor.call(context,pageName,...);
    if (needIntercept) {
      return Future.value(null);
    } else {
      return super.openPage(context,pageName,...);
    }
  }
}

例如:在打开大厅页面之前,判断用户是否登录,如未登录则跳转到登录页面。

mixin HomeRouteContainer on MixinRouterInterceptContainer {
  @override
  Map<String, WidgetBuilder> installRouters() {
    registerRouteInterceptor('/home', (...) => if(!isLogin) openLoginPage());  //注册拦截路由表
    Map<String, WidgetBuilder> originRoutes = super.installRouters();
    Map<String, WidgetBuilder> newRoutes = {};
    newRoutes['/home'] = (context) => HomePage();
    newRoutes.addAll(originRoutes);
    return newRoutes;
  }
}

情况二:

说明:为了通过外链能打开对应的页面,很多项目都是Url统跳。

解决方案:只需要对原有的 AppRouteContainer 进行扩展,代理默认的页面打开方法,实现url解析:

class AppRouteContainer extends MixinRouterContainer
    with HomeRouteContainer, SettingRouteContainer {  //通过mixin机制粘合项目各个路由模块
    
   Future<T?>? urlToPage<T>(BuildContext context, String urlStr, ...) {
  	Uri? url = Uri.tryParse(urlStr);
  	if (url == null) return Future.error('parse url fail');
  	Map<String, String> args = {};
    args.addAll(url.queryParameters);
    args['_url'] = urlStr;
    String pageName = url.host;
    super.openPage(context,'/' + pageName ...);
  }
}

在进行url统跳时,调用urlToPage即可打开相应的flutter页面。

5、深入探索

对项目的路由改造到此就结束了么?回过头来再想想,发现还是存在一些问题:

  • 需要手动创建并维护不同的路由管理模块(HomeRouteContainer、SettingRouteContainer)
  • 新的页面都需要在对应的模块类中进行手动注册

客户端原生项目对于这类问题,可以通过注解的方式解决,类似阿里的ARouter,那么Flutter也可以借鉴此方式完成进一步的优化,利用注解去生成对应的路由模块管理文件,避免手动维护,具体如下:

5.1、注解子路由表

const String HOME_ROUTE_TABLE = 'HomeRouteTable';
const String SETTING_ROUTE_TABLE = 'SettingsRouteTable';

//tDescription: 仅仅作为生成类的注释
@RouterTableList(
  tableList: [
    RouterTable(tName: HOME_ROUTE_TABLE, tDescription: '大厅路由模块'),
    RouterTable(tName: SETTING_ROUTE_TABLE, tDescription: '设置路由模块'),
  ],
)


//with HomeRouterTable, MineRouterTable,即上面声明的两个路由表的名字
class AppRouteContainer extends MixinRouterInterceptContainer
    with HomeRouteTable, SettingsRouteTable {
  AppRouteContainer._();

  static AppRouteContainer _instance = AppRouteContainer._();

  static AppRouteContainer get share => _instance;
}

5.2、注解普通路由

//方式一:不使用路由参数
@MixinRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_a')
class APage extends StatelessWidget {
  const APage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('APage'),
    );
  }
}

//方式二:使用路由参数
@MixinRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_a', arg=true)
class APage extends StatelessWidget {
  const APage(dynamic args, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('APage'),
    );
  }
}

5.3、注解拦截路由

@MixinRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_b')
class BPage extends StatelessWidget {
  const BPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('BPage'),
    );
  }
}

@MixinInterceptRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_b')
bool interceptorMinePage(context, pageName, pushType, {arguments, predicate}) { //函数签名固定写法
  print('toLogin');
  return true;
}

6、集成使用

在项目的pubspec.yaml中添加依赖,即可开启注解路由之旅

dependencies:
  flutter:
    sdk: flutter
  flutter_mixin_router: ^1.0.0      # 添加路由模块管理基类
  flutter_mixin_router_ann: 1.0.0   # 添加注解类

dev_dependencies:
  build_runner: 2.1.8               # 添加依赖
  flutter_mixin_router_gen: 1.0.1   # 添加代码生成工具库

在项目页面上添加对应的注解后,执行以下命令生成对应的路由代码

# 清除增量编译缓存
flutter packages pub run build_runner clean

# 重新生成代码
flutter packages pub run build_runner build --delete-conflicting-outputs