大部分應用程序都有多個屏幕或頁面,並希望用戶能從當前屏幕平滑過渡到另一個屏幕,Flutter的路由和導航功能可以幫助我們管理應用程序中的用戶界面之間的命名和過渡。
管理多個用戶界面有兩個核心概念和類:路由(Route)和導航器(Navigator),路由(Route)是應用程序的“屏幕”或“頁面”的抽象,導航器(Navigator)是管理路由的控件。導航器(Navigator)可以推送(push)和彈出(pop)路由來幫助用戶從當前屏幕移動到另一個屏幕。
使用導航器
移動應用通常通過稱為“屏幕”或“頁面”的全屏元素顯示其內容,在Flutter中,這些元素稱為路由,它們由導航器(Navigator)控件管理。導航器管理一組路由(Route)對象,並提供了管理堆棧的方法,例如Navigator.push和Navigator.pop。
Future push(
BuildContext context,
Route route
)
// 將給定的路由添加到最靠近給定上下文的導航器的歷史記錄,並過渡到它
bool pop(
BuildContext context, [
result
])
// 從導航器中彈出最靠近給定上下文的路由
顯示屏幕路由
雖然可以直接創建導航器,但最常見的是使用由WidgetsApp或MaterialApp控件創建的導航器,可以使用Navigator.of引用該導航器。
NavigatorState of(
BuildContext context
)
// 最接近該類的實例包含給定上下文的狀態
一個MaterialApp是最簡單的設置方式,MaterialApp的home成為導航器堆棧底部的路由。要在堆棧上推送(push)一 個新路由,您可以創建一個具有構建器功能的MaterialPageRoute實例,它可以創建您想要顯示在屏幕上的任何內容。
MaterialPageRoute是一種模態路由,可以通過平台自適應過渡來切換屏幕。對於Android,頁面推送過渡時向上滑動頁面,並將其淡入淡出,彈出過渡則向下滑動頁面,該過渡適應平台。在iOS上,頁面從右側滑入,反向彈出,當另一個頁面進入以覆蓋時,該頁面也會在視差中向左移動。
默認情況下,當模態路由替換為另一路由時,上一個路由將保留在內存中,要在沒有必要時釋放所有資源,可以將maintainState設置為false。
現在我們新建一個項目myapp,然后用下面代碼替換main.dart文件的代碼:
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: '應用程序首頁'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _openNewPage() {
setState(() {
Navigator.of(context).push(new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('新的頁面')
),
body: new Center(
child: new Text(
'點擊浮動按鈕返回首頁',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
Navigator.of(context).pop();
},
child: new Icon(Icons.replay),
),
);
},
));
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'點擊浮動按鈕打開新頁面',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
通常沒有必要提供一個使用Scaffold在路由中彈出導航器的控件,因為Scaffold會自動在其AppBar中添加一個后退按鈕。按下后退按鈕會導致Navigator.pop被調用,在Android上按系統后退按鈕也是一樣的。
使用命名導航器路由
移動應用程序經常管理大量路由,通常可以通過名稱來引用它們。路由名稱按慣例使用類似路徑的結構,例如“/a/b/c”,應用程序的主頁路由默認為“/”。
可以使用Map<String, WidgetBuilder>創建MaterialApp,該Map映射從路由的名稱到將創建它的構建器,MaterialApp使用此映射為導航器的onGenerateRoute回調創建一個值。
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '應用程序首頁'),
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => new MyPage(title: 'A 頁面'),
'/b': (BuildContext context) => new MyPage(title: 'B 頁面'),
'/c': (BuildContext context) => new MyPage(title: 'C 頁面')
},
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Row(
children: <Widget>[
new RaisedButton(
child: new Text('A按鈕'),
onPressed: () { Navigator.of(context).pushNamed('/a'); },
),
new RaisedButton(
child: new Text('B按鈕'),
onPressed: () { Navigator.of(context).pushNamed('/b'); },
),
new RaisedButton(
child: new Text('C按鈕'),
onPressed: () { Navigator.of(context).pushNamed('/c'); },
)
]
)
);
}
}
class MyPage extends StatelessWidget {
MyPage({Key key, this.title}) : super(key: key);
final String title;
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title)
),
);
}
}
Map<String, WidgetBuilder> routes是應用程序的頂級路由表,當使用Navigator.pushNamed推送命名路由時,將在此映射中查找路由名稱。如果名稱存在,則相關聯的WidgetBuilder將用於構造一個MaterialPageRoute,該新的路由執行適當的過渡。
如果應用程序只有一個頁面,那么可以使用home指定它,如果指定了home,那么在此映射中為Navigator.defaultRouteName提供路由是一個錯誤。如果請求沒有在此表中指定的路由(或通過home),則會調用onGenerateRoute回調來構建頁面。
路由可以返回一個值
當路由被推送,用於獲取用戶的輸入時,可以通過pop方法的result參數返回用戶的輸入值。推送路由的方法返回Future類型,Future將在路由彈出時解析,而Future的值是pop方法的result參數。
例如,如果我們要求用戶按“確定”確認操作,我們可以等待Navigator.push的結果:
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '應用程序首頁'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _result = false;
Future<Null> _openNewPage() async {
bool value = await Navigator.of(context).push(new MaterialPageRoute<bool>(
builder: (BuildContext context) {
return new Center(
child: new GestureDetector(
child: new Text("確定"),
onTap: () { Navigator.of(context).pop(true); },
),
);
}
));
setState(() {
_result = value;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'用戶當前選擇為 $_result',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
如果用戶按“確定”,則值將為真。如果用戶退出路由,例如通過按Scaffold的后退按鈕,該值將為null。當使用路由return值時,路由的type參數必須與pop的結果類型相匹配。這就是為什么我們使用MaterialPageRoute
彈出路由
路由不一定需要掩蓋整個屏幕,PopupRoutes覆蓋屏幕,屏幕顏色只能部分不透明,以允許當前屏幕顯示。彈出路由是模態的,因為它們阻止了下面的控件的輸入。
框架有創建和顯示彈出路由的功能,例如:showDialog、showMenu和showModalBottomSheet。這些功能如上所述返回其推送的路由的Future,調用時可以等待返回的值在路由彈出時采取行動,或者發現路由的值。
還有一些創建彈出路由的控件,如PopupMenuButton和DropdownButton。這些控件創建PopupRoute的內部子類,並使用路由的push和pop方法來顯示和關閉它們。
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '應用程序首頁'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _result = false;
Future<Null> _openNewPage() async {
bool value = await showDialog(
context: context,
barrierDismissible: true,
child: new Center(
child: new GestureDetector(
child: new Text("確定"),
onTap: () { Navigator.of(context).pop(true); },
),
)
);
setState(() {
_result = value;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'用戶當前選擇為 $_result',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
定制路由
您可以創建自己的控件庫路由類,比如PopupRoute、ModalRoute或PageRoute的子類,以控制用於顯示路由,包括路由模態屏障的顏色和行為以及路由其他方面的動畫轉換。
PageRouteBuilder類可以根據回調來定義自定義路由,下面是一個示例,當路由出現或消失時,它會旋轉和淡化其子對象。此路由不會遮蔽整個屏幕,因為它指定了opaque: false,就像彈出路由一樣。
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '應用程序首頁'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _openNewPage() {
Navigator.of(context).push(new PageRouteBuilder(
opaque: false,
pageBuilder: (BuildContext context, _, __) {
return new Center(child: new Text('定制頁面路由'));
},
transitionsBuilder: (_, Animation<double> animation, __, Widget child) {
return new FadeTransition(
opacity: animation,
child: new RotationTransition(
turns: new Tween<double>(begin: 0.5, end: 1.0).animate(animation),
child: child,
),
);
}
));
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'點擊浮動按鈕打開定制頁面',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
頁面路由分為兩部分:“頁面”和“過渡”。頁面成為傳遞給buildTransitions方法的子代的后代,通常,頁面僅構建一次,因為它不依賴於其動畫參數,在此示例中為_和__。過渡建立在每個幀的持續時間。