# 路由
前端人眼中的路由:
- 基础路由:从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
安装依赖。
# 基础示例
- 创建路由文件
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
页面切换效果定义;
- 在
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,
);
}
}
测试复杂参数的传递:
- 先定义一个数据类型:
class TestParams {
final int id;
final String name;
TestParams(this.id, this.name);
}
- 参数传递
FluroRouter router = getIt<FluroRouter>();
router.navigateTo(context, '/home', routeSettings: RouteSettings(
arguments: TestParams(1, 'test your name')
));
- 取参数
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
# 基础示例
- 新建 两个测试 页面
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(),
],
),
),
),
);
}
}
- 定义路由
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 {}
- 执行自动路由构建命令
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';
}
- 改造
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(),
);
}
}
- 使用导航:
全局注册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参数
- 设置路由文件:
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 {}
- 重新运行自动生成路由的命令,
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;
}
- 传参:
final router = getIt<AppRouter>();
router.push(HomeViewRoute(id: 3, username: 'hello world'));
// 路径传参 + Query传参
// router.pushNamed('/home-view/10?username=toimc');
- 取参:
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'),
),
);
}
}
# 导航守卫
需要调整路由路径,把routes.dart移动到
lib/routes/routes.dart
,并删除routes.gr.dart
创建
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); } } }
调整
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 {}
修改登录业务逻辑:
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的状态判断。
# Navigator 2.0
# 为什么需要新的 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的设计思想。
# Navigator 1.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'),
),
),
);
}
}