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: () {
//路由pop弹出
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',
//home: MainRoute(),
//注册路由表
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: () {
//路由pop弹出
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(
///Tween:在补间动画中,定义开始点结束点
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: () {
//路由pop弹出
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: () {
//路由pop弹出
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.push内部其实就是 Navigator.of(context).push
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: () {
//路由pop弹出
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: () {
//路由pop弹出
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(
///指定路由器widget的key
navigatorKey: navigatorKey,
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
///输出Navigator
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: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}

在创建Navigator的时候,会给一个key,这个key可以看成一个Widget的id。这里的**_navigator就是我们指定的navigatorKey**(如果我们没指定,会给默认值的,所以不要疑惑不指定是不是就不创建Navigator了)。而通过这个key,就能够获得这个Navigator。直接获得了路由自然不需要再去查找了!