前言
这篇文章我通过页面跳转、路由传参、命名路由、路由生成钩子的顺序从简入深的说明Flutter的路由。
想要了解Flutter的路由就要了解MaterialPageRoute和Navigator
MaterialPageRoute
MaterialPageRoute
继承自PageRoute
类,PageRoute
类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute
是 Material 组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
- 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
- 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
MaterialPageRoute
构造函数的各个参数的意义
MaterialPageRoute({ WidgetBuilder builder, RouteSettings settings, bool maintainState = true, bool fullscreenDialog = false, }) |
参数 | 意义 |
---|---|
builder | 是一个 WidgetBuilder 类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。 |
settings | 包含路由的配置信息,如路由名称、是否初始路由(首页)。 |
maintainState | 默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState 为false |
fullscreenDialog | 示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果fullscreenDialog 为true ,新页面将会从屏幕底部滑入(而不是水平方向)。 |
Navigator
Navigator
是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator
通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator
提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:
常用方法
方法 | 描述 |
---|---|
Future push(BuildContext context, Route route) | 将给定的路由入栈(即打开新的页面),返回值是一个Future 对象,用以接收新路由出栈(即关闭)时的返回数据。 |
bool pop(BuildContext context, [ result ]) | 将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据。 |
Navigator
还有很多其它方法,如Navigator.replace
、Navigator.popUntil
等
实例方法
Navigator 类中第一个参数为 context 的静态方法都对应一个 Navigator 的实例方法, 比如Navigator.push(BuildContext context, Route route)
等价于Navigator.of(context).push(Route route)
,下面命名路由相关的方法也是一样的
路由管理
页面跳转
通过Navigator.push( context,
MaterialPageRoute(builder: (context) {return NewRoute();}));来实现跳转到NewRoute界面
//导包,此行代码作用是导入了 Material UI 组件库。Material (opens new window)是一种标准的移动端和 web 端的视觉设计语言, Flutter默认提供了一套丰富的 Material 风格的 UI 组件。 import 'package:flutter/material.dart'; //应用入口 void main() { runApp(const MyApp()); } /// 在 Flutter 中,大多数东西都是 widget(后同“组件”或“部件”),包括对齐(alignment)、填充(padding)和布局(layout)等,它们都是以 widget 的形式提供。 class MyApp extends StatelessWidget { const MyApp({super.key}); @override ///Flutter 在构建页面时,会调用组件的build方法,widget 的主要工作是提供一个 build()方法来描述如何构建 UI 界面(通常是通过组合、拼装其它基础 widget)。 Widget build(BuildContext context) { ///MaterialApp 是 Material 库中提供的 Flutter APP 框架,通过它可以设置应用的名称、主题、语言、首页及路由列表等。MaterialApp也是一个 widget。 return MaterialApp( //用于名称 title: 'Flutter Demo', //主题 theme: ThemeData( primarySwatch: Colors.blue, ), //home 为 Flutter 应用的首页,它也是一个 widget。 home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); final title = 'home界面'; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(title), ), body: Center( child: Row( children: [ Text(title), TextButton( style: TextButton.styleFrom( backgroundColor: Colors.white, textStyle: const TextStyle(color: Colors.white)), onPressed: () { //导航到新路由 Navigator.push(context, MaterialPageRoute(builder: (context) { return const NewRoute(); })); }, child: const Text("跳转到测试界面"), ), ], )), ); } } class NewRoute extends StatelessWidget { const NewRoute({super.key}); final title = '路由测试'; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(title), ), body: Center( child: Row( children: [ Text(title), TextButton( style: TextButton.styleFrom( backgroundColor: Colors.white, textStyle: const TextStyle(color: Colors.white)), onPressed: () { //导航到新路由 Navigator.push(context, MaterialPageRoute(builder: (context) { return const MyApp(); })); }, child: const Text("跳转回去"), ), ], )), ); } } |
我们只要点击按钮就可以进行跳转了
命名路由
所谓“命名路由”(Named Route)即有名字的路由,我们可以先给路由起一个名字,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式。
路由表
要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名字与哪个路由组件相对应。其实注册路由表就是给路由起名字,路由表的定义如下:
Map<String, WidgetBuilder> routes; |
它是一个Map
,key 为路由的名字,是个字符串;value 是个builder
回调函数,用于生成相应的路由 widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder
回调函数,然后调用该回调函数生成路由 widget 并返回。
注册路由表
路由表的注册方式很简单,我们在MyApp
类的build
方法中找到MaterialApp
,添加routes
属性,代码如下:
MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), //注册路由表 routes:{ "new_page":(context) => NewRoute(), ... // 省略其它路由注册信息 } , home: MyHomePage(title: 'Flutter Demo Home Page'), ); |
现在我们就完成了路由表的注册。上面的代码中home
路由并没有使用命名路由,如果我们也想将home
注册为命名路由应该怎么做呢?其实很简单,直接看代码:
MaterialApp( title: 'Flutter Demo', initialRoute:"/", //名为"/"的路由作为应用的home(首页) theme: ThemeData( primarySwatch: Colors.blue, ), //注册路由表 routes:{ "new_page":(context) => NewRoute(), "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由 } ); |
通过路由名打开新路由页
要通过路由名称来打开新路由,可以使用Navigator
的pushNamed
方法:
要通过路由名称来打开新路由,可以使用Navigator 的pushNamed方法: |
Navigator
除了pushNamed
方法,还有pushReplacementNamed
等其他管理命名路由的方法。接下来我们通过路由名来打开新的路由页,修改FlatButton
的onPressed
回调代码,改为:
onPressed: () { Navigator.pushNamed(context, "new_page"); //Navigator.push(context, // MaterialPageRoute(builder: (context) { // return NewRoute(); //})); }, |
命名路由参数传递
先注册一个路由:
routes:{ "new_page":(context) => EchoRoute(), } , |
在路由页通过RouteSetting
对象获取路由参数:
class EchoRoute extends StatelessWidget { @override Widget build(BuildContext context) { //获取路由参数 var args=ModalRoute.of(context).settings.arguments; //...省略无关代码 } } |
在打开路由时传递参数
Navigator.of(context).pushNamed("new_page", arguments: "hi"); |
适配
假设我们也想将上面路由传参示例中的TipRoute
路由页注册到路由表中,以便也可以通过路由名来打开它。但是,由于TipRoute
接受一个text
参数,我们如何在不改变TipRoute
源码的前提下适配这种情况?其实很简单:
MaterialApp( ... //省略无关代码 routes: { "tip2": (context){ return TipRoute(text: ModalRoute.of(context).settings.arguments); }, }, ); |
路由生成钩子
|
假设我们要开发一个电商 APP,当用户没有登录时可以看店铺、商品等信息,但交易记录、购物车、用户个人信息等页面需要登录后才能看。为了实现上述功能,我们需要在打开每一个路由页前判断用户登录状态!如果每次打开路由前我们都需要去判断一下将会非常麻烦,那有什么更好的办法吗?路由生成钩子就能解决这个问题
MaterialApp
有一个onGenerateRoute
属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)
打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder
函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute
来生成路由。onGenerateRoute
回调签名如下:
Route<dynamic> Function(RouteSettings settings) |
有了onGenerateRoute
回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute
回调,然后在该回调中进行统一的权限控制,如:
MaterialApp( ... //省略无关代码 onGenerateRoute:(RouteSettings settings){ return MaterialPageRoute(builder: (context){ String routeName = settings.name; // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由, // 引导用户登录;其它情况则正常打开路由。 } ); } ); |