title: Flutter 路由与导航date: 2021-07-18 15:07:48.692
updated: 2021-07-18 15:25:28.879
url: /?p=318
categories: Flutter
tags:
Flutter 路由与导航
路由(Route)的概率大概就是大航海时代,我们如果需要去一个目的地,我们就得查路由,确定航向。在编程中,我们时常听到路由这个词汇,其实是一个道理,所以一般会有一个路由表,从路由表查路径什么的,路由也可直接理解为路径。
路由管理
路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。这和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: MainRoute(), ); } }
class MainRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("主页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondRoute(); })); }, child: Text("进入第二页"), ) ], ), ); } }
class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第二页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.pop(context); }, child: Text("返回"), ) ], ), ); } }
|
命名路由
命名路由(Named Route)即给路由起一个名字,然后可以通过路由名字直接打开新的路由。这为路由管理带来了一种直观、简单的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', routes: { "/" :(context) => MainRoute(), "new_page": (context) => SecondRoute(), }, ); } }
class MainRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("主页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () async { var result = await Navigator.pushNamed(context, "new_page"); debugPrint("返回:$result"); }, child: Text("进入第二页"), ) ], ), ); } }
class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第二页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.pop(context, "结束"); }, child: Text("返回"), ) ], ), ); } }
|
命名路由的最大优点是直观,我们可以通过语义化的字符串来管理路由。但其有一个明显的缺点:不能直接传递路由参数。假设SecondRoute,需要接受一个字符串参数tip
,然后再在屏幕中心将tip
的内容显示出来。因为命名路由需要提前注册到路由表中,所以就无法动态修改tip
参数。
自定义路由切换动画
Material库中提供了MaterialPageRoute,它在Android上会上下滑动切换。如果想自定义路由切换动画,可以使用PageRouteBuilder。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: MainRoute(), ); } }
class MainRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("主页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () async { var result = await Navigator.push( context, PageRouteBuilder( transitionDuration: Duration(milliseconds: 500), pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return SlideTransition( position: new Tween<Offset>( begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0), ).animate(animation), child: SecondRoute(), ); }, ), ); debugPrint("返回:$result"); }, child: Text("进入第二页"), ) ], ), ); } }
class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第二页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.pop(context, "结束"); }, child: Text("返回"), ) ], ), ); } }
|
同时我们也可以对动画进行组合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: MainRoute(), ); } }
class MainRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("主页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () async { var result = await Navigator.push( context, PageRouteBuilder( transitionDuration: Duration(milliseconds: 500), pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return new FadeTransition( opacity: animation, child: new RotationTransition( turns: new Tween<double>(begin: 0.5, end: 1.0) .animate(animation), child: SecondRoute(), ), ); },), ); debugPrint("返回:$result"); }, child: Text("进入第二页"), ) ], ), ); } }
class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第二页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.pop(context, "结束"); }, child: Text("返回"), ) ], ), ); } }
|
注意点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text("主页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.of(context).push(MaterialPageRoute(builder: (_){ return new SecondRoute(); })); }, child: Text("进入第二页"), ) ], ), ), ); } }
class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第二页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.pop(context); }, child: Text("返回"), ) ], ), ); } }
|
这段代码运行会出现错误:
问题关键点在于**Navigator operation requested with a context that does not include a Navigator.**(导航操作请求使用了不包含Navigator的上下文context)
Navigator
实际上也是一个Widget,这个异常出现在Navigator.of(context)
路由器的获取上,而这句代码会**从当前的context的父级一层层向上去查找一个Navigator
**,我们当前传递的context就是MyApp,它的父级是root——UI根节点。Navigator
这个widget的并不是由root创建的,因此在root下一级的上下文中无法获得Navigator
。
在之前所有的路由案例中,我们的上下文是MainRoute,它的父级是MaterialApp。MaterialApp内部就会创建一个Navigator。
MaterialApp->_MaterialAppState->WidgetsApp->_WidgetsAppState
所以问题就在于,Navigator
需要通过MaterialApp或者它孩子的上下文。
解决一
按照此笔记最开始的正常路由演示案例来进行修改。
解决二
使用Builder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text("主页"), ), body: Column( children: <Widget>[ Text("第一个页面"), Builder(builder: (context){ return RaisedButton( onPressed: () { Navigator.of(context).push(MaterialPageRoute(builder: (_){ return new SecondRoute(); })); }, child: Text("进入第二页"), ); }) ], ), ), ); } }
class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第二页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.pop(context); }, child: Text("返回"), ) ], ), ); } }
|
使用Builder嵌套,Builder的参数可以看成一个回调,接收自身的context并返回布局配置。现在路由是从Builder的父亲开始查找啦,自然能找到Navigator。
解决三
使用navigatorKey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); @override Widget build(BuildContext context) { return new MaterialApp( navigatorKey: navigatorKey, title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text("主页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { debugPrint(navigatorKey.currentWidget.runtimeType.toString()); navigatorKey.currentState.push(MaterialPageRoute(builder: (_){ return new SecondRoute(); })); }, child: Text("进入第二页"), ) ], ), ), ); } }
class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第二页"), ), body: Column( children: <Widget>[ Text("第一个页面"), RaisedButton( onPressed: () { Navigator.pop(context); }, child: Text("返回"), ) ], ), ); } }
|
在创建Navigator
的时候,会给一个key,这个key可以看成一个Widget的id。这里的**_navigator就是我们指定的navigatorKey**(如果我们没指定,会给默认值的,所以不要疑惑不指定是不是就不创建Navigator
了)。而通过这个key,就能够获得这个Navigator
。直接获得了路由自然不需要再去查找了!