# 路由

前端人眼中的路由:

  • 基础路由:从A->B->C想怎么跳怎么跳,有一个统一的方法;
  • 路由传参(路径参数、query参数等);
  • 路由守卫,路由生命周期钩子方法;

Flutter中难道就没有路由方案吗?是因为Navigator1.0时代的问题:

  • 原先的路由只能由InitialRoute来决定,路径传参无法实现;
  • 简单的Push与Pop,不能直接操作路由堆栈;
  • 所有的嵌套的逻辑,只能回到根Navigator响应;

路由的解决方案:

  • 第三方包:fluro,flutter_modular,auto_route;
  • Navigator 2.0 方案,是官方改写过后的(不推荐,上手学习坡度大);

# fluro

官网:https://pub.dev/packages/fluro (opens new window)

特点:函数式导航、路由参数解析、内置路由过渡

# 安装

dependencies:
  fluro: ^2.0.3

使用flutter pub get安装依赖。

# 基础示例

  1. 创建路由文件
import 'package:fluro/fluro.dart';
import 'package:my_app/pages/home_view.dart';
import 'package:my_app/pages/login_view.dart';

class Routes {
  // 1.定义页面的路径
  static String login = '/';
  static String home = '/home';

  // 2.定义对应路径的->handler
  static Handler _loginHandler = Handler(
    handlerFunc: (context, parameters) {
      return LoginView();
    },
  );
  static Handler _homeHandler = Handler(
    handlerFunc: (context, parameters) {
      return HomeView();
    },
  );

  // 3.定义configureRoutes -> 用于产生页面的路由
  static void configureRoutes(FluroRouter router) {
    router.define(login, handler: _loginHandler);
    router.define(home,
        handler: _homeHandler, transitionType: TransitionType.cupertino);
  }
}

说明:

  • 每个路由对应一个处理函数;
  • 处理函数对应一个页面;
  • configureRoutes定义路径与处理函数的对应关系,transitionType: TransitionType.cupertino页面切换效果定义;
  1. main.dart中配置路由

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    // 1.实例化router -> FluroRouter
    FluroRouter router = FluroRouter();
    // 2.configureRoutes -> 配置路由
    Routes.configureRoutes(router);
    // 3.MaterialApp -> router.generator
    // CuperinoApp
    return MaterialApp(
      theme: ThemeData(
        brightness: Brightness.light,
        primaryColorBrightness: Brightness.light,
      ),
      // 这里不需要使用home,应该交由fluro来管理我们的应用
      // home: Scaffold(
      //   appBar: AppBar(),
      //   // 2. SafeArea
      //   body: HomePage(),
      // ),
      initialRoute: Routes.login,
      onGenerateRoute: router.generator,
    );
    // return MaterialApp(
    //   home: Scaffold(
    //     appBar: AppBar(),
    //     body: Container(
    //       child: Text(
    //         'toimc社区',
    //         textDirection: TextDirection.ltr,
    //       ),
    //     ),
    //   ),
    // );
  }
}

# 路径参数

路径参数与前端中的路径参数意思一样:

设置方法:

class Routes {
  // 1.定义页面的路径
  static String login = '/';
  // 路径参数
  static String home = '/home/:id/:username';
  // ...
}

取值方法:

class Routes {
// ...
	// 2.定义对应路径的->handler
  static Handler _loginHandler = Handler(
    handlerFunc: (context, params) {
      // 可以从parameters中取参
      // print('params is $params')
      return LoginView();
    },
  );
}

# 编辑式导航

需要把fluro的实例进行全局实例化,进行使用。

借助get_it:

import 'package:fluro/fluro.dart';
import 'package:get_it/get_it.dart';
import 'package:my_app/services/content_service.dart';
import 'package:my_app/services/user_service.dart';

final getIt = GetIt.instance;

void setupGetIt() {
  getIt.registerLazySingleton<UserService>(() => UserService());
  getIt.registerLazySingleton<ContentService>(() => ContentService());
  getIt.registerSingleton<FluroRouter>(FLuroRouter());
}

修改main.dart

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    // 修改这里
    FluroRouter router = getIt<FluroRouter>();

    Routes.configureRoutes(router);
    return MaterialApp(
      theme: ThemeData(
        brightness: Brightness.light,
        primaryColorBrightness: Brightness.light,
      ),
      initialRoute: Routes.login,
      onGenerateRoute: router.generator,
    );
  }
}

测试复杂参数的传递:

  1. 先定义一个数据类型:
class TestParams {
  final int id;
  final String name;

  TestParams(this.id, this.name);
}
  1. 参数传递
FluroRouter router = getIt<FluroRouter>();
router.navigateTo(context, '/home', routeSettings: RouteSettings(
  arguments: TestParams(1, 'test your name')
));
  1. 取参数
static Handler _homeHandler = Handler(
  handlerFunc: (context, params) {
    final args = context?.settings?.arguments as TestParams;
    print('args is 👉 $args');
    return HomeView();
  },
);

# auto_route

自动路由方案。

# 安装

dependencies:
  flutter:
    sdk: flutter

  # 自动路由
  auto_route: ^2.4.2
  

dev_dependencies:
  flutter_test:
    sdk: flutter

  # 自动路由
  auto_route_generator: ^2.4.1

# 基础示例

  1. 新建 两个测试 页面

lib/pages/home_view.dart

import 'package:flutter/material.dart';

class HomeView extends StatelessWidget {
  const HomeView({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Text('HomeView'),
      ),
    );
  }
}

lib/pages/login_view.dart

import 'package:flutter/material.dart';
import 'package:my_app/widgets/login/login_form.dart';

class LoginView extends StatefulWidget {
  const LoginView({
    Key? key,
  }) : super(key: key);

  
  State<LoginView> createState() => _LoginViewState();
}

class _LoginViewState extends State<LoginView> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: SafeArea(
        // 1.Column -> logo + form
        // 2.如何引用图片 -> 图片圆角
        child: SingleChildScrollView(
          child: Column(
            children: [
              Container(
                margin: EdgeInsets.only(
                  top: 50.0,
                  bottom: 30.0,
                ),
                height: 120,
                decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [
                  BoxShadow(
                    color: Colors.black12,
                    spreadRadius: 3,
                    blurRadius: 10,
                  ),
                ]),
                child: Center(
                  child: CircleAvatar(
                    backgroundImage: AssetImage('static/images/logo.png'),
                    radius: 60,
                    backgroundColor: Colors.white,
                  ),
                ),
              ),
              LoginForm(),
            ],
          ),
        ),
      ),
    );
  }
}
  1. 定义路由libs/routes.dart
import 'package:auto_route/auto_route.dart';
import 'package:my_app/pages/home_view.dart';
import 'package:my_app/pages/login_view.dart';

(routes: <AutoRoute>[
  AutoRoute(page: LoginView, initial: true),
  AutoRoute(page: HomeView),
])
class $AppRouter {}
  1. 执行自动路由构建命令flutter pub run build_runner build,产生对应的.gr.dart文件

如果冲突,可以使用--delete-conflicting-outputs,注意使用该参数之前,一定要git提交代码。

自动路由文件libs/routes.gr.dart

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// AutoRouteGenerator
// **************************************************************************

import 'package:auto_route/auto_route.dart' as _i3;
import 'package:flutter/material.dart' as _i4;

import 'pages/home_view.dart' as _i2;
import 'pages/login_view.dart' as _i1;

class AppRouter extends _i3.RootStackRouter {
  AppRouter([_i4.GlobalKey<_i4.NavigatorState>? navigatorKey])
      : super(navigatorKey);

  
  final Map<String, _i3.PageFactory> pagesMap = {
    LoginViewRoute.name: (routeData) {
      return _i3.MaterialPageX<dynamic>(
          routeData: routeData, child: const _i1.LoginView());
    },
    HomeViewRoute.name: (routeData) {
      return _i3.MaterialPageX<dynamic>(
          routeData: routeData, child: const _i2.HomeView());
    }
  };

  
  List<_i3.RouteConfig> get routes => [
        _i3.RouteConfig(LoginViewRoute.name, path: '/'),
        _i3.RouteConfig(HomeViewRoute.name, path: '/home-view')
      ];
}

/// generated route for [_i1.LoginView]
class LoginViewRoute extends _i3.PageRouteInfo<void> {
  const LoginViewRoute() : super(name, path: '/');

  static const String name = 'LoginViewRoute';
}

/// generated route for [_i2.HomeView]
class HomeViewRoute extends _i3.PageRouteInfo<void> {
  const HomeViewRoute() : super(name, path: '/home-view');

  static const String name = 'HomeViewRoute';
}
  1. 改造main.dart文件
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:my_app/routes.gr.dart';
import 'package:my_app/setup_get_it.dart';

void main() async {
  setupGetIt();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final _rootRouter = getIt<AppRouter>();
    return MaterialApp.router(
      routeInformationParser: _rootRouter.defaultRouteParser(),
      routerDelegate: _rootRouter.delegate(),
    );
  }
}
  1. 使用导航:

全局注册AppRouter

import 'package:get_it/get_it.dart';
import 'package:my_app/routes.gr.dart';
import 'package:my_app/services/content_service.dart';
import 'package:my_app/services/user_service.dart';

final getIt = GetIt.instance;

void setupGetIt() {
  getIt.registerLazySingleton<UserService>(() => UserService());
  getIt.registerLazySingleton<ContentService>(() => ContentService());
  // 全局注册,以便在全局使用
  getIt.registerSingleton<AppRouter>(AppRouter());
}

比如在login_form.dart中:

Padding(
  padding: const EdgeInsets.only(top: 10.0),
  child: SizedBox(
    width: 120,
    child: ElevatedButton(
      child: Text('登录'),
      onPressed: () async {
        final router = getIt<AppRouter>();
        router.push(HomeViewRoute());
        // 1.AutoRouter 方法一
        // AutoRouter.of(context).navigate(HomeViewRoute());
        // AutoRouter.of(context).navigateNamed('/home-view');

        // 2.context.router 方法二
        // context.router.navigate(HomeViewRoute());

        // 3.context.pushRoute 方法三
        // context.pushRoute(HomeViewRoute());

        // String mobile = controller.text;
        // String code = codeController.text;
        // if (RegExp(RegParttern.mobileParttern).hasMatch(mobile) &&
        //     RegExp(RegParttern.codeParttern).hasMatch(code)) {
        //   await userService.login(mobile, code);
        // } else {
        //   CommonToast.showToast("手机号或验证码格式不正确");
        // }
      },
    ),
  ),
)

# 路径&Query参数

  1. 设置路由文件:
import 'package:auto_route/auto_route.dart';
import 'package:my_app/pages/home_view.dart';
import 'package:my_app/pages/login_view.dart';

(routes: <AutoRoute>[
  AutoRoute(page: LoginView, initial: true),
  AutoRoute(page: HomeView, path: "/home-view/:id"),
])
class $AppRouter {}
  1. 重新运行自动生成路由的命令,
flutter pub run build_runner build --delete-conflicting-outputs

或者使用--watch参数

flutter pub run build_runner watch --delete-conflicting-outputs

产生出来的文件:

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// AutoRouteGenerator
// **************************************************************************

import 'package:auto_route/auto_route.dart' as _i3;
import 'package:flutter/material.dart' as _i4;

import 'pages/home_view.dart' as _i2;
import 'pages/login_view.dart' as _i1;

class AppRouter extends _i3.RootStackRouter {
  AppRouter([_i4.GlobalKey<_i4.NavigatorState>? navigatorKey])
      : super(navigatorKey);

  
  final Map<String, _i3.PageFactory> pagesMap = {
    LoginViewRoute.name: (routeData) {
      return _i3.MaterialPageX<dynamic>(
          routeData: routeData, child: const _i1.LoginView());
    },
    HomeViewRoute.name: (routeData) {
      final pathParams = routeData.pathParams;
      final queryParams = routeData.queryParams;
      final args = routeData.argsAs<HomeViewRouteArgs>(
          orElse: () => HomeViewRouteArgs(
              id: pathParams.optInt('id'),
              username: queryParams.optString('username')));
      return _i3.MaterialPageX<dynamic>(
          routeData: routeData,
          child: _i2.HomeView(
              key: args.key,
              id: args.id,
              username: args.username,
              testParam: args.testParam));
    }
  };

  
  List<_i3.RouteConfig> get routes => [
        _i3.RouteConfig(LoginViewRoute.name, path: '/'),
        _i3.RouteConfig(HomeViewRoute.name, path: '/home-view/:id')
      ];
}

/// generated route for [_i1.LoginView]
class LoginViewRoute extends _i3.PageRouteInfo<void> {
  const LoginViewRoute() : super(name, path: '/');

  static const String name = 'LoginViewRoute';
}

/// generated route for [_i2.HomeView]
class HomeViewRoute extends _i3.PageRouteInfo<HomeViewRouteArgs> {
  HomeViewRoute(
      {_i4.Key? key, int? id, String? username, _i2.TestParam? testParam})
      : super(name,
            path: '/home-view/:id',
            args: HomeViewRouteArgs(
                key: key, id: id, username: username, testParam: testParam),
            rawPathParams: {'id': id},
            rawQueryParams: {'username': username});

  static const String name = 'HomeViewRoute';
}

class HomeViewRouteArgs {
  const HomeViewRouteArgs({this.key, this.id, this.username, this.testParam});

  final _i4.Key? key;

  final int? id;

  final String? username;

  final _i2.TestParam? testParam;
}
  1. 传参:
 final router = getIt<AppRouter>();
router.push(HomeViewRoute(id: 3, username: 'hello world'));

// 路径传参 + Query传参
// router.pushNamed('/home-view/10?username=toimc');
  1. 取参:
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

class TestParam {
  final int id;
  final String username;

  TestParam(this.id, this.username);
}

class HomeView extends StatelessWidget {
  final int? id;
  final String? username;
  // 复杂参数的传递
  final TestParam? testParam;

  const HomeView(
      {Key? key,
      ('id') this.id,
      ('username') this.username,
      this.testParam})
      : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        margin: const EdgeInsets.only(top: 110.0),
        child: Text('HomeView$id - $username'),
      ),
    );
  }
}

# 导航守卫

  1. 需要调整路由路径,把routes.dart移动到lib/routes/routes.dart,并删除routes.gr.dart

  2. 创建AuthGuard,文件路径lib/routes/auth_guard.dart

    import 'package:auto_route/auto_route.dart';
    import 'package:my_app/routes/routes.gr.dart';
    import 'package:my_app/setup_get_it.dart';
    import 'package:my_app/storage.dart';
    
    class AuthGuard extends AutoRouteGuard {
      
      void onNavigation(NavigationResolver resolver, StackRouter router) {
        bool _authentication = getIt<Storage>().getToken() != null;
        // TODO: implement onNavigation
        if (_authentication) {
          resolver.next();
        } else {
          if (router.current.name != LoginViewRoute.name) {
            router.push(LoginViewRoute());
          }
          resolver.next(false);
        }
      }
    }
    
  3. 调整routes.dart文件,添加AuthGuard到对应的路由:

    import 'package:auto_route/auto_route.dart';
    import 'package:my_app/pages/home_view.dart';
    import 'package:my_app/pages/login_view.dart';
    import 'package:my_app/routes/auth_guard.dart';
    
    (routes: <AutoRoute>[
      AutoRoute(page: LoginView, initial: true),
      AutoRoute(page: HomeView, path: "/home-view/:id", guards: [AuthGuard]),
    ])
    class $AppRouter {}
    
  4. 修改登录业务逻辑:

    String mobile = controller.text;
    String code = codeController.text;
    if (RegExp(RegParttern.mobileParttern).hasMatch(mobile) &&
        RegExp(RegParttern.codeParttern).hasMatch(code)) {
      HttpResponse httpResponse =
        await userService.login(mobile, code);
      if (httpResponse.code == 200) {
        // token进行设置 -> authGuard authentication
        //  说明用户登录成功
        final router = getIt<AppRouter>();
        // router.pushNamed('/home-view/10?username=toimc');
        router.push(HomeViewRoute(id: 3));
      }
    } else {
      CommonToast.showToast("手机号或验证码格式不正确");
    }
    

    说明:

    • 用户点击登录,发送请求;
    • 请求响应后,获取响应的数据,并设置token;
    • 通过Storage来持久化token信息;
    • 通过导航守卫来进行token的状态判断。

# 为什么需要新的 API

在探究具体细节之前,我们有必要了解一下 Flutter 团队为什么要不惜这些代价对 Navigator API 做这次的重构,主要有如下几点原因。

  • 原始 API 中的 initialRoute 参数,即系统默认的初始页面,在应用运行后就不能再更改了。这种情况下,如果用户接收到一个系统通知,点击后想要从当前的路由栈状态 [Main -> Profile -> Settings] 重启切换到新的 [Main -> List -> Detail[id=24] 路由栈,旧的 Navigator API 并没有一种优雅的实现方式实现这种效果。
  • 原始的命令式 Navigator API 只提供给了开发者一些非常针对性的接口,如 push()pop() 等,而没有给出一种更灵活的方式让我们直接操作路由栈。这种做法其实与 Flutter 理念相违背,试想如果我们想要改变某个 widget 的所有子组件只需要重建所有子组件并且创建一系列新的 widget 即可,而将此概念应用在路由中,当应用中存在一系列路由页面并想要更改时,我们只能调用 push()pop() 这类接口来回操作, 这样的 Flutter 食之无味
  • 嵌套路由下,手机设备自带的回退按钮只能由根 Navigator 响应。在目前的应用中,我们很多场景都需要在某个子 tab 内单独管理一个子路由栈。假设有这个场景,用户在子路由栈中做一系列路由操作之后,点击系统回退按钮,消失的将是整个上层的根路由,我们当然可以使用某种措施来避免这种状况,但归咎起来,这也不应该是应用开发者应该考虑的问题。

Navigator 2.0 新增的声明式 API 主要包含 Page (opens new window) API、Router (opens new window) API 两个部分。

与 Flutter 中 Widget、Element、 RenderObject 三棵树的概念保持一致。

重要说明,可以参考官方文档 (opens new window)学习Navigator2.0的设计思想。

import 'package:flutter/material.dart';

void main() {
  runApp(const Nav2App());
}

class Nav2App extends StatelessWidget {
  const Nav2App({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: TextButton(
          child: const Text('查看子页面'),
          onPressed: () {
            // 跳转子页面
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) {
                return const DetailScreen();
              }),
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  const DetailScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: TextButton(
          child: const Text('弹出'),
          onPressed: () {
            // 跳出子页面
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

# 命名式路由

import 'package:flutter/material.dart';

void main() {
  runApp(const Nav2App());
}

class Nav2App extends StatelessWidget {
  const Nav2App({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    // 命名式路由
    return MaterialApp(
      // home: HomeScreen(),
      // 静态的路由表
      routes: {
        '/': (context) => HomeScreen(),
        '/detail': (context) => DetailScreen(),
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: TextButton(
          child: const Text('查看子页面'),
            Navigator.pushNamed(context, '/detail');
            // Navigator.push(
            //   context,
            //   MaterialPageRoute(builder: (context) {
            //     return const DetailScreen();
            //   }),
            // );
          },
        ),
      ),
    );
  }
}

# 路由传参

核心点:

  • 动态路由

    onGenerateRoute: (settings) {
      // 通过setttings来判断当前的页面与返回的内容
      if (settings.name == '/') {
        return MaterialPageRoute(builder: (context) => HomeScreen());
      }
      // ... 
    },
    
  • 参数获取

    onGenerateRoute: (settings) {
      // ...
      // /detail/:id
      var uri = Uri.parse(settings.name!);
      if (uri.pathSegments.length == 2 &&
          uri.pathSegments.first == 'detail') {
        var id = uri.pathSegments[1];
        return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
      }
    }
    

完整代码:

import 'package:flutter/material.dart';

void main() {
  runApp(const Nav2App());
}

class Nav2App extends StatelessWidget {
  const Nav2App({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    // 命名式路由
    return MaterialApp(
      // home: HomeScreen(),
      // 静态的路由表
      // routes: {
      //   '/': (context) => HomeScreen(),
      //   '/detail': (context) => DetailScreen(),
      // },
      onGenerateRoute: (settings) {
        if (settings.name == '/') {
          return MaterialPageRoute(builder: (context) => HomeScreen());
        }
        // /detail/:id
        var uri = Uri.parse(settings.name!);
        if (uri.pathSegments.length == 2 &&
            uri.pathSegments.first == 'detail') {
          var id = uri.pathSegments[1];
          return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
        }
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: TextButton(
          child: const Text('查看子页面'),
          onPressed: () {
            // /detail/:id
            Navigator.pushNamed(context, '/detail/1');
            // Navigator.push(
            //   context,
            //   MaterialPageRoute(builder: (context) {
            //     return const DetailScreen();
            //   }),
            // );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  final String id;

  const DetailScreen({Key? key, required this.id}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: TextButton(
          child: Text('弹出$id'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

# 学习建议

  • 如果不需要Flutter去兼容web端,直接使用navigator1.0即可;
  • 如果需要兼容web端,那么请使用第三方路由方案,效率更高;

# 防止用户退出页面

使用WillPopScope组件来阻止用户退出或者是右滑左边框的操作:

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

class TestParam {
  final int id;
  final String username;

  TestParam(this.id, this.username);
}

class HomeView extends StatelessWidget {
  final int? id;
  final String? username;
  final TestParam? testParam;

  const HomeView(
      {Key? key,
      ('id') this.id,
      ('username') this.username,
      this.testParam})
      : super(key: key);

  
  Widget build(BuildContext context) {
    // 防止用户右滑左边框的操作:
    return WillPopScope(
      onWillPop: () async => false,
      child: Scaffold(
        body: Container(
          margin: const EdgeInsets.only(top: 110.0),
          child: Text('HomeView$id - $username'),
        ),
      ),
    );
  }
}