vlambda博客
学习文章列表

【手把手学习flutter】flutter的路由管理

一、前言

VS Code创建一个新的Flutter工程,命名为"firstflutterapp"。创建好后,就会得到一个计数器应用的Demo。

我们先运行创建的工程,效果如图2-1所示:

主要Dart代码是在 lib/main.dart 文件中,下面是它的源码:

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


  2. void main() => runApp(new MyApp());


  3. class MyApp extends StatelessWidget {

  4. @override

  5. Widget build(BuildContext context) {

  6. return new MaterialApp(

  7. title: 'Flutter Demo',

  8. theme: new ThemeData(

  9. primarySwatch: Colors.blue,

  10. ),

  11. home: new MyHomePage(title: 'Flutter Demo Home Page'),

  12. );

  13. }

  14. }


  15. class MyHomePage extends StatefulWidget {

  16. MyHomePage({Key key, this.title}) : super(key: key);

  17. final String title;


  18. @override

  19. _MyHomePageState createState() => new _MyHomePageState();

  20. }


  21. class _MyHomePageState extends State<MyHomePage> {

  22. int _counter = 0;


  23. void _incrementCounter() {

  24. setState(() {

  25. _counter++;

  26. });

  27. }


  28. @override

  29. Widget build(BuildContext context) {

  30. return new Scaffold(

  31. appBar: new AppBar(

  32. title: new Text(widget.title),

  33. ),

  34. body: new Center(

  35. child: new Column(

  36. mainAxisAlignment: MainAxisAlignment.center,

  37. children: <Widget>[

  38. new Text(

  39. 'You have pushed the button this many times:',

  40. ),

  41. new Text(

  42. '$_counter',

  43. style: Theme.of(context).textTheme.headline,

  44. ),

  45. ],

  46. ),

  47. ),

  48. floatingActionButton: new FloatingActionButton(

  49. onPressed: _incrementCounter,

  50. tooltip: 'Increment',

  51. child: new Icon(Icons.add),

  52. ), // This trailing comma makes auto-formatting nicer for build methods.

  53. );

  54. }

  55. }

二、路由管理

flutter中的路由指的是页面,与web开发中route概念基本一样,router在安卓中通常指的是一个activity,在ios中指的viewController。

所谓的路由管理,就是管理页面之间如果跳转。flutter中的路由管理与原生开发类似,无论是android还是ios,路由管理都会维护一个路由栈,路由栈push操作对应打开一个新页面,路由出栈pop操作对应的关闭操作,路由管理主要是指如何管理路由栈。

在上面的计数器的基础上

2.1一个简单例子

1、创建新路由

创建新路由,命名为”newRoutePage“。

 
   
   
 
  1. // 新路由

  2. class NewRoutePage extends StatelessWidget {

  3. @override

  4. Widget build(BuildContext context) {

  5. return Scaffold(

  6. appBar: AppBar(

  7. title: Text("新页面1"),

  8. ),

  9. body: Center(

  10. child: Text('这是一个新页面1'),

  11. ),

  12. );

  13. }

  14. }

新路由继承StatelessWidget,界面里居中写”这是一个新页面1“,title为”新页面“

2、添加新链接按钮

在_MyHomePageState.build方法中的column的子widget中添加一个按钮FlatButton。

 
   
   
 
  1. class _MyHomePageState extends State<MyHomePage> {

  2. ...

  3. @override

  4. Widget build(BuildContext context) {

  5. return new Scaffold(

  6. appBar: new AppBar(

  7. title: new Text(widget.title),

  8. ),

  9. body: new Center(

  10. child: new Column(

  11. mainAxisAlignment: MainAxisAlignment.center,

  12. children: <Widget>[

  13. new Text(

  14. 'You have pushed the button this many times:',

  15. ),

  16. new Text(

  17. '$_counter',

  18. style: Theme.of(context).textTheme.headline,

  19. ),

  20. FlatButton(

  21. child: Text('打开新页面'),

  22. textColor: Colors.blue,

  23. onPressed: () {

  24. Navigator.push(context, MaterialPageRoute(builder: (context){

  25. return NewRoutePage();

  26. }));

  27. },

  28. )

  29. ],

  30. ),

  31. ),

  32. floatingActionButton: new FloatingActionButton(

  33. onPressed: _incrementCounter,

  34. tooltip: 'Increment',

  35. child: new Icon(Icons.add),

  36. ), // This trailing comma makes auto-formatting nicer for build methods.

  37. );

  38. }

  39. }

这样我们添加一个新开路由的按钮,并将文字颜色设置为蓝色,打开新路由页面。

【手把手学习flutter】flutter的路由管理

【手把手学习flutter】flutter的路由管理

2.2 MaterialPageRoute类

MaterialPageRoute继承PageRoute类,pageRoute类是一个抽象类,表示占有整个屏幕空间的一个「模态路由页面」,他还定义了路由构建及切换时过渡动画的相关接口及属性。

MaterialPageRoute是Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画一致的路由切换动画:

  • 对于android:当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当期页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。

  • 对于ios:当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。

我们看一下MaterialPageRoute构造函数的各个参数的意义。

 
   
   
 
  1. MaterialPageRoute({

  2. WidgetBuilder builder,

  3. RouteSettings settings,

  4. bool maintainState = true,

  5. bool fullscreenDialog = false,

  6. })

  • builder 是一个WidgetBuilder类型的回调函数,它的作用是侯建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。

  • settings 包含路由的配置信息,如路由的名称,是否初始路由(首页)。

  • maintainState:默认情况下,当入栈一个新路由的时候,原来的路由仍会被保存在内存中,如果想在路由没有用的时候释放其所占的所有资源,可以设置maintainState为false。

  • fullscreenDialog 表示新的路由页面是否是一个全屏的模态框,在ios中,如果fullscreenDialog为true,新页面会从屏幕底部滑入(而不是水平方向)。

2.3 Navigator

Navigator是一个路由管理的组件,提供路由打开的方法。

Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面时栈顶的路由,Navigator提供一系列的方法管理路由栈,常见的方法:

  • push Future push(BuildContext context, Route route) 将给定的路由入栈(即打开新页面),返回一个Future对象,接收新路由出栈时的返回数据。

  • pop bool pop(BuildContext context, [result])将栈顶路由出栈,result为页面关闭时返回给上一个页面的数据。

  • 其他 比如,Navigator.replace, Navigator.popUntil等

2.4 路由传值

路由跳转的时候,我们需要在路由上携带一些参数,比如打开商品详情页,我们需要携带一个商品id,这样商品详情页才知道展示哪个商品信息;等等

我们先来一个简单的例子,需求:

  • 我们从RouterParamsTestA(简称:A页面)跳到RouterParamsTestB(B页面),即A跳到B。

  • A跳到B,需要携带参数 页面传参值,A->B

  • B页面有个返回按钮,返回到A页面,返回参数 返回值saucxsB->A

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


  2. void main() => runApp(new MyApp());


  3. class MyApp extends StatelessWidget {

  4. @override

  5. Widget build(BuildContext context) {

  6. return new MaterialApp(

  7. title: 'Flutter Demo',

  8. theme: new ThemeData(

  9. primarySwatch: Colors.blue,

  10. ),

  11. home: new RouterParamsTestA(),

  12. );

  13. }

  14. }


  15. // 新路由Test

  16. class RouterParamsTestA extends StatelessWidget {

  17. @override

  18. Widget build(BuildContext context) {

  19. return Center(

  20. child: RaisedButton(

  21. onPressed: () async {

  22. var result = await Navigator.push(

  23. context,

  24. MaterialPageRoute(builder: (context){

  25. return RouterParamsTestB(params: '页面传参值, A->B',);

  26. })

  27. );

  28. print('路由返回值:$result');

  29. },

  30. child: Text('打开提示页RouterParamsTestB'),

  31. ),

  32. );

  33. }

  34. }


  35. // 新路由 路由传值

  36. class RouterParamsTestB extends StatelessWidget {

  37. RouterParamsTestB({

  38. Key key,

  39. @required this.params, // 接收一个text参数

  40. }) : super(key: key);

  41. final String params;


  42. @override

  43. Widget build(BuildContext context) {

  44. return Scaffold(

  45. appBar: AppBar(title: Text('提示'),),

  46. body: Padding(

  47. padding: EdgeInsets.all(18),

  48. child: Center(

  49. child: Column(children: <Widget>[

  50. Text(params),

  51. RaisedButton(

  52. onPressed: () => Navigator.pop(context, '返回值saucxs,B->A'),

  53. child: Text('返回A页面'),

  54. )

  55. ],)

  56. ),

  57. ),

  58. );

  59. }

  60. }

运行上面代码,点击「打开提示页RouterParamsTestB」按钮跳到B页面,在B页面点击「返回A页面」按钮跳转回到A页面。

【手把手学习flutter】flutter的路由管理

【手把手学习flutter】flutter的路由管理

说明一下:

  • B页面文案“页面传参值, A->B'”是A页面通过params参数传递给新路由页B。我们可以通过等待Navigator.push(…)返回的Future来获取新路由的返回数据。

  • B页面有两种返回方式,第一种是直接点击导航栏上的返回箭头,第二种点解页面的「返回A页面」按钮。这两种方式区别是前者不会返回数据给上一个路由,后者会。

 
   
   
 
  1. Restarted application in 887ms.

  2. I/flutter ( 5465): 路由返回值:null

  3. I/flutter ( 5465): 路由返回值:返回值saucxsB->A

介绍的是非命名路由的传值方式,命名路由的传值方式会有所不同。

2.5 命名路由

命名路由意思就是给每一个路由起一个名字,然后通过路由名字直接打开新的路由,这样和一些框架的路由配置项一样,直接简单方式管理路由。

1、路由表

要想使用命名路由,必须注册一个路由表,这样使名字和路由组件相对应,其实注册路由表就是给路由起名字,路由表定义如下:

 
   
   
 
  1. Map<String, WidgetBuilder> routes;

首先这是一个Map,key为路由名字,字符串;value为是builder回调函数,用于生成相应的路由widget。

我们在通过路由名字打开新路由时,应用会根据路由在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

2、注册路由表

路由表的注册方式很简单,我们还是回到「计数器」的示例,然后在MyApp类的build方法中找到MaterialApp,添加routes属性,代码如下:

 
   
   
 
  1. class MyApp extends StatelessWidget {

  2. @override

  3. Widget build(BuildContext context) {

  4. return new MaterialApp(

  5. title: 'Flutter Demo',

  6. theme: new ThemeData(

  7. primarySwatch: Colors.blue,

  8. ),

  9. // 注册路由表

  10. routes: {

  11. "new_page": (context) => NewRoutePage(),

  12. // 其他路由信息

  13. },

  14. home: new MyHomePage(title: 'Fultter Demo',),

  15. );

  16. }

  17. }

这就完成了路由表注册,上面代码中的home路由并没有使用命名路由,我们可以将home注册为命名路由应该怎么做?show me code

 
   
   
 
  1. class MyApp extends StatelessWidget {

  2. @override

  3. Widget build(BuildContext context) {

  4. return new MaterialApp(

  5. title: 'Flutter Demo',

  6. theme: new ThemeData(

  7. primarySwatch: Colors.blue,

  8. ),

  9. // 注册路由表

  10. routes: {

  11. "new_page": (context) => NewRoutePage(),

  12. "/":(context) => MyHomePage(title: 'Fultter Demo'), //注册首页路由

  13. },

  14. );

  15. }

  16. }

其实可以看到路由表中注册一下MyHomePage路由,然后将其名字作为MaterialApp的initialRoute属性值即可,该属性决定应用的初始路由是哪一个命名路由。

3、通过路由名打开新路由页

通过路由名称打开新路由,可以使用Navigator的pushNamed方法:

 
   
   
 
  1. Future pushNamed(BuildContext context, String routeName,{Object arguments})

Navigator 除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法。

修改FlatButton的onPressed回调代码,改为:

 
   
   
 
  1. FlatButton(

  2. child: Text('打开新页面'),

  3. textColor: Colors.blue,

  4. onPressed: () {

  5. Navigator.pushNamed(context, 'new_page');

  6. // Navigator.push(context, MaterialPageRoute(builder: (context){

  7. // return NewRoutePage();

  8. // }));

  9. },

  10. ),

然后热重载,点击「打开新页面」按钮,依然可以打开路由页。

4、命名路由参数传递

在初始的版本中,命名路由是不能传递参数的,后来才支持了参数,下面是命名路由如何传递并获得路由参数。

首先我们注册一个路由:

 
   
   
 
  1. routes: {

  2. 'new_page': (context) => EchoRoute(),

  3. },

在路由页通过RouteSetting对象获取路由参数:

 
   
   
 
  1. class EchoRoutePage extends StatelessWidget {

  2. @override

  3. Widget build(BuildContext context) {

  4. //获取路由参数

  5. var args=ModalRoute.of(context).settings.arguments;

  6. print('获取路由参数:$args');

  7. return Scaffold(

  8. appBar: AppBar(

  9. title: Text("一个新页面"),

  10. ),

  11. body: Center(

  12. child: Text('命名路由参数传递'),

  13. )

  14. );

  15. }

  16. }

控制行提示输出:

 
   
   
 
  1. I/flutter ( 5465): 获取路由参数:saucxs

在打开路由时传递参数

 
   
   
 
  1. FlatButton(

  2. child: Text('打开新页面EchoPage'),

  3. textColor: Colors.blue,

  4. onPressed: () {

  5. Navigator.of(context).pushNamed('echo_page', arguments: 'saucxs');

  6. },

  7. ),

【手把手学习flutter】flutter的路由管理

5、适配

如果我们想上面路由传参示例中的RouterParamsTestB路由注册到路由表中,以便可以通过路由名来打开,但是,由于RouterParamsTestB接收一个params参数,我们如何在不改变RouterParamsTestB源码的前提下适配这种情况,很简单:

 
   
   
 
  1. routes: {

  2. "new_page": (context) => NewRoutePage(),

  3. "echo_page": (context) => EchoRoutePage(),

  4. "testB": (context) {

  5. return RouterParamsTestB(params: ModalRoute.of(context).settings.arguments);

  6. },

  7. "/":(context) => RouterParamsTestA(), //注册首页路由

  8. },

三、路由生成钩子

还有路由生成钩子,MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下:

 
   
   
 
  1. Route<dynamic> Function(RouteSettings settings)

有了onGenerateRoute回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制。

四、总结

我们总结一下,学习了Flutter中路由管理,传参的方式,以及命名路由的内容。实际开发中到底哪一种「路由管理方式」。

使用命名路由的管理方式,好处:

  • 语义化更明确

  • 代码更好维护,如果使用匿名路由,必须在调用Navigator.push的地方创建新路由页,这样不仅需要import新路由页的dart文件,而且这样的代码会比较分散。

  • 可以通过OnGenerateRoute做一些全局的路由跳转前置处理逻辑。

还有一些关于路由管理的内容我们没有介绍,比如路由MaterialApp中还有navigatorObservers和onUnknownRoute两个回调属性,前者可以监听所有路由跳转动作,后者在打开一个不存在的命名路由时会被调用,由于这些功能并不常用,而且也比较简单。

五、欢迎关注

show me code:https://github.com/saucxs/flutter_learning/tree/master/hellotest