Flutter 路由封装

334 阅读3分钟

曾经写了关于Fluro的路由配置,但Fluro目前已经很久不维护了,因此考虑使用flutter原生的路由。原文章:juejin.cn/post/729456…

前置知识

回调onGenerateRoute

  • MaterialApp或者CupertinoApp,接收onGenerateRoute参数,是用于处理路由生成的回调函数,它可以根据路由名称动态生成页面
  • 用户使用Navigator路由跳转方法时,便会触发onGenerateRoute
  • onGenerateRoute回调函数接收RouteSettings参数,其中可以通过settings.name获取路由的名称
  • 根据路由名称返回RouteMaterialPageRoute最终继承自Route
  • MaterialPageRoute中的builder,即可返回你的页面
MaterialApp(
  onGenerateRoute: (RouteSettings settings) {
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (context) => HomePage());
      case '/details':
        return MaterialPageRoute(builder: (context) => DetailsPage());
      default:
        return MaterialPageRoute(builder: (context) => NotFoundPage());
    }
  },
);

使用

Navigator.pushNamed(context, '/details');

路由传参数

  • RouteSettings settings中可以获取arguments参数,这就是路由参数
MaterialApp(
  onGenerateRoute: (RouteSettings settings) {
    switch (settings.name) {
      //...
      case '/details':
        String? id = settings.arguments as String?;
        return MaterialPageRoute(builder: (context) => DetailsPage(id:id));
      //...
    }
  },
);

使用

Navigator.pushNamed(context, '/details',arguments: 'id123');

封装

封装onGenerateRoute

  • 封装onGenerateRoute回调方法,通过map获取对应的页面
  • lib/router/create_routes.dart
//...

typedef WidgetBuilder = Widget Function(Object? arguments);

class CreateRoutes {
  static final routes = <String, WidgetBuilder>{
    '/': (arguments) => const IndexPage(),
    '/tokenDetail': (arguments) => TokenDetailPage(token: arguments as String),
    '/auditDetail': (arguments) => const AuditDetailPage(),
  };

  static Route<dynamic>? generateRoute(RouteSettings settings) {
    // 根据路由名称返回对应的页面Widget,并传递参数
    // widget不存在则返回404页面
    final Widget widget =
        routes[settings.name]?.call(settings.arguments) ?? const Page404();
    return MaterialPageRoute(builder: (context) => widget);
  }
}

lib/main.dart

 MaterialApp(
  onGenerateRoute: CreateRoutes.generateRoute,
),

封装路由无context跳转

  • navigatorKey 是访问和控制 Navigator 的键(GlobalKey<NavigatorState>)。它允许你在应用的任意地方访问 Navigator,从而实现导航操作。

main.dart

MaterialApp(
  onGenerateRoute: CreateRoutes.generateRoute,
  navigatorKey: navigatorKey // navigatorKey
),

lib/router/nav_ctrl.dart

import 'package:flutter/material.dart';

// 全局key,用于无context跳转的情况
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

class NavCtrl {
  // 无context跳转
  static Future<dynamic> push(
    String path, {
    Object? arguments,
  }) {
    return Navigator.of(navigatorKey.currentContext!)
        .pushNamed(path, arguments: arguments);
  }

  // 无context replase
  static Future<dynamic> replease(
    String path, {
    Object? arguments,
  }) {
    return Navigator.of(navigatorKey.currentContext!)
        .pushReplacementNamed(path, arguments: arguments);
  }

  // 无context,清空路由栈跳转,一般用于跳转首页这种情况
  static Future<dynamic> switchTab(
    String path, {
    Object? arguments,
  }) {
    return Navigator.of(navigatorKey.currentContext!)
        .pushNamedAndRemoveUntil(path, (_) => false, arguments: arguments);
  }

  // 无context返回,并指定路由返回多少层,默认返回上一页面, 返回带参数arguments
  static void back({int count = 1, Object? arguments}) {
    NavigatorState state = Navigator.of(navigatorKey.currentContext!);
    while (count-- > 0) {
      if (state.canPop()) state = state..pop(arguments);
    }
  }
}

使用

NavCtrl.push('/tokenDetail', arguments: "USDT");

自动生成路由名的变量

以上步骤已经完成了对路由的封装,下面只是优化,可不做

  • 我们在使用时是写的字符串,这样没有代码提示,且容易写错,因此我们可以单独用变量来存储字符串

例如:

class Routes {
  static const String home = '/';
  static const String tokenDetail = '/tokenDetail';
  static const String auditDetail = '/auditDetail';
}

使用:

NavCtrl.push(Routes.tokenDetail, arguments: "USDT");

但每次都去写这个class比较麻烦,因此编写脚本自动生成Routes class

脚本

步骤:

  1. 通过路由文件,提取路由名称
  2. 拼接格式,生成代码,输出到文件

lib/router/script.dart

import 'dart:io';

void main() {
  // 获取路由文件内容
  var path = './lib/router/create_routes.dart';
  var res = File(path).readAsStringSync();

  // 提取路由名称
  RegExp regExp = RegExp("['"]/(.*?)['"]: "); // 匹配引号中的内容
  Iterable<Match> matches = regExp.allMatches(res);
  var list = matches.map((match) => match.group(1)).toList();

  // 拼接格式:static const String step1 = '/step1';
  final String s = list.map((e) {
    if (e?.isEmpty ?? true) {
      return "  static const String home = '/$e';";
    }
    return "  static const String $e = '/$e';";
  }).join('\n');

  // 替换模版
  String template = '''
class Routes {
%s
}
''';
  template = template.replaceAll("%s", s);

  // 写入文件
  File('./lib/router/routes.dart').writeAsStringSync(template);
}

运行

dart ./lib/router/script.dart

运行时自动启动

在.vscode文件夹中新增文件tasks.json

  • 任务配置文件必须命名为 tasks.json

.vscode/tasks.json

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "generate routers",
      "type": "shell",
      "command": "dart ./lib/router/script.dart",
      "presentation": {
        "echo": true,
        "reveal": "always",
      },
    }
  ]
}
  • 在launch.json添加运行前任务 preLaunchTask

.vscode/launch.json

{
  "configurations": [
    {
      "name": "Flutter (dev)",
      "request": "launch",
      "preLaunchTask": "generate routers", 
      "type": "dart",
      "args": [
        "--dart-define",
        "APP_ENV=dev"
      ]
    },
    {
      "name": "Flutter (pro)",
      "request": "launch",
      "preLaunchTask": "generate routers", 
      "type": "dart",
      "args": [
        "--dart-define",
        "APP_ENV=pro"
      ]
    },
    {
      "name": "Flutter (test)",
      "request": "launch",
      "preLaunchTask": "generate routers", 
      "type": "dart",
      "args": [
        "--dart-define",
        "APP_ENV=test"
      ]
    }
  ]
}

这样运行就会自动生成文件了