大部分應用程序都包含多個頁面,并希望用戶能從當前屏幕平滑過渡到另一個屏幕。移動應用程序通常通過被稱為“屏幕”或“頁面”的全屏元素來顯示內容。在 Flutter 中,這些元素被稱為路由(Route),它們由導航器(Navigator)控件管理。導航器管理著路由對象的堆棧并提供管理堆棧的方法,如 Navigator.push
和 Navigator.pop
,通過路由對象的進出棧來使用戶從一個頁面跳轉到另一個頁面。
查看示例代碼。
基本用法
Navigator 的基本用法,從一個頁面跳轉到另一個頁面,通過第二頁面上的返回按鈕回到第一個頁面。
創建兩個頁面
首先創建兩個頁面,每個頁面包含一個按鈕。點擊第一個頁面上的按鈕將導航到第二個頁面。點擊第二個頁面上的按鈕將返回到第一個頁面。初始時顯示第一個頁面。
// main.dart
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Navigation',
home: new FirstScreen(),
);
}
}
// demo1_navigation.dart
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('First Screen'),
),
body: new Center(
child: new RaisedButton(
child: new Text('Launch second screen'),
onPressed: null,
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Second Screen'),
),
body: new Center(
child: new RaisedButton(
child: new Text('Go back!'),
onPressed: null,
),
),
);
}
}
跳轉到第二頁面
為了導航到新的頁面,我們需要調用 Navigator.push 方法。該方法將添加 Route 到路由棧中!
我們可以直接使用 MaterialPageRoute 創建路由,它是一種模態路由,可以通過平臺自適應的過渡效果來切換屏幕。默認情況下,當一個模態路由被另一個替換時,上一個路由將保留在內存中,如果想釋放所有資源,可以將 maintainState
設置為 false
。
給第一個頁面上的按鈕添加 onPressed
回調:
onPressed: () {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new SecondScreen()),
);
},
返回第一個頁面
Scaffold
控件會自動在 AppBar
上添加一個返回按鈕,點擊該按鈕會調用 Navigator.pop
。
現在希望點擊第二個頁面中間的按鈕也能回到第一個頁面,添加回調函數,調用 Navigator.pop
:
onPressed: () {
Navigator.pop(context);
}
頁面跳轉傳值
在進行頁面切換時,通常還需要將一些數據傳遞給新頁面,或是從新頁面返回數據。考慮此場景:我們有一個文章列表頁,點擊每一項會跳轉到對應的內容頁。在內容頁中,有喜歡和不喜歡兩個按鈕,點擊任意按鈕回到列表頁并顯示結果。
我會接著上面的例子繼續編寫。
定義 Article 類
首先我們創建一個 Article 類,擁有兩個屬性:標題、內容。
class Article {
String title;
String content;
Article({this.title, this.content});
}
創建列表頁面和內容頁面
列表頁面中初始化 10 篇文章,然后使用 ListView
顯示它們。
內容頁面標題顯示文章的標題,主體部分顯示內容。
class ArticleListScreen extends StatelessWidget {
final List<Article> articles = new List.generate(
10,
(i) => new Article(
title: 'Article $i',
content: 'Article $i: The quick brown fox jumps over the lazy dog.',
),
);
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Article List'),
),
body: new ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
return new ListTile(
title: new Text(articles[index].title),
);
},
),
);
}
}
class ContentScreen extends StatelessWidget {
final Article article;
ContentScreen(this.article);
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('${article.title}'),
),
body: new Padding(
padding: new EdgeInsets.all(15.0),
child: new Text('${article.content}'),
),
);
}
}
跳轉到內容頁并傳遞數據
接下來,當用戶點擊列表中的文章時將跳轉到ContentScreen
,并將 article 傳遞給 ContentScreen
。
為了實現這一點,我們將實現 ListTile
的 onTap 回調。 在的 onTap 回調中,再次調用Navigator.push
方法。
return new ListTile(
title: new Text(articles[index].title),
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new ContentScreen(articles[index]),
),
);
},
);
內容頁返回數據
在內容頁底部添加兩個按鈕,點擊按鈕時跳轉會列表頁面并傳遞參數。
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('${article.title}'),
),
body: new Padding(
padding: new EdgeInsets.all(15.0),
child: new Column(
children: <Widget>[
new Text('${article.content}'),
new Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
new RaisedButton(
onPressed: () {
Navigator.pop(context, 'Like');
},
child: new Text('Like'),
),
new RaisedButton(
onPressed: () {
Navigator.pop(context, 'Unlike');
},
child: new Text('Unlike'),
),
],
)
],
),
),
);
}
修改 ArticleListScreen
列表項的 onTap
回調,處理內容頁面返回的數據并顯示。
onTap: () async {
String result = await Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new ContentScreen(articles[index]),
),
);
if (result != null) {
Scaffold.of(context).showSnackBar(
new SnackBar(
content: new Text("$result"),
duration: const Duration(seconds: 1),
),
);
}
},
定制路由
通常,我們可能需要定制路由以實現自定義的過渡效果等。定制路由有兩種方式:
- 繼承路由子類,如:PopupRoute、ModalRoute 等。
- 使用 PageRouteBuilder 類通過回調函數定義路由。
下面使用 PageRouteBuilder 實現一個頁面旋轉淡出的效果。
onTap: () async {
String result = await Navigator.push(
context,
new PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 1000),
pageBuilder: (context, _, __) =>
new ContentScreen(articles[index]),
transitionsBuilder:
(_, Animation<double> animation, __, Widget child) =>
new FadeTransition(
opacity: animation,
child: new RotationTransition(
turns: new Tween<double>(begin: 0.0, end: 1.0)
.animate(animation),
child: child,
),
),
));
if (result != null) {
Scaffold.of(context).showSnackBar(
new SnackBar(
content: new Text("$result"),
duration: const Duration(seconds: 1),
),
);
}
},
命名導航器路由
通常,移動應用管理著大量的路由,并且最容易的是使用名稱來引用它們。路由名稱通常使用路徑結構:“/a/b/c”,主頁默認為 “/”。
創建 MaterialApp
時可以指定 routes
參數,該參數是一個映射路由名稱和構造器的 Map。MaterialApp
使用此映射為導航器的 onGenerateRoute 回調參數提供路由。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Navigation',
initialRoute: '/',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => new ArticleListScreen(),
'/new': (BuildContext context) => new NewArticle(),
},
);
}
}
路由的跳轉時調用 Navigator.pushNamed
:
Navigator.of(context).pushNamed('/new');
這里有一個問題就是使用 Navigator.pushNamed
時無法直接給新頁面傳參數,目前官方還沒有標準解決方案,我知道的方案是在 onGenerateRoute
回調中利用 URL 參數自行處理。
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
if (settings.name == '/') {
builder = (BuildContext context) => new ArticleListScreen();
} else {
String param = settings.name.split('/')[2];
builder = (BuildContext context) => new NewArticle(param);
}
return new MaterialPageRoute(builder: builder, settings: settings);
},
// 通過 URL 傳遞參數
Navigator.of(context).pushNamed('/new/xxx');
嵌套路由
一個 App 中可以有多個導航器,將一個導航器嵌套在另一個導航器下面可以創建一個內部的路由歷史。例如:App 主頁有底部導航欄,每個對應一個 Navigator,還有與主頁處于同一級的全屏頁面,如登錄頁面等。接下來,我們實現這樣一個路由結構。
添加 Home 頁面
添加 Home 頁面,底部導航欄切換主頁和我的頁面。
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _HomeState();
}
}
class _HomeState extends State<Home> {
int _currentIndex = 0;
final List<Widget> _children = [
new PlaceholderWidget('Home'),
new PlaceholderWidget('Profile'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _children[_currentIndex],
bottomNavigationBar: new BottomNavigationBar(
onTap: onTabTapped,
currentIndex: _currentIndex,
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.home),
title: new Text('Home'),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.person),
title: new Text('Profile'),
),
],
),
);
}
void onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
}
class PlaceholderWidget extends StatelessWidget {
final String text;
PlaceholderWidget(this.text);
@override
Widget build(BuildContext context) {
return new Center(
child: new Text(text),
);
}
}
效果如下:
然后我們將 Home 頁面組件使用 Navigator 代替,Navigator 中有兩個路由頁面:home 和 demo1。home 顯示一個按鈕,點擊按鈕調轉到前面的 demo1 頁面。
import 'package:flutter/material.dart';
import './demo1_navigation.dart';
class HomeNavigator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Navigator(
initialRoute: 'home',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case 'home':
builder = (BuildContext context) => new HomePage();
break;
case 'demo1':
builder = (BuildContext context) => new ArticleListScreen();
break;
default:
throw new Exception('Invalid route: ${settings.name}');
}
return new MaterialPageRoute(builder: builder, settings: settings);
},
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
),
body: new Center(
child: new RaisedButton(
child: new Text('demo1'),
onPressed: () {
Navigator.of(context).pushNamed('demo1');
},
),
),
);
}
}
效果如下圖:
可以看到,點擊按鈕跳轉到 demo1 頁面后,底部的 tab 欄并沒有消失,因為這是在子導航器中進行的跳轉。要想顯示全屏頁面覆蓋底欄,我們需要通過根導航器進行跳轉,也就是 MaterialApp
內部的導航器。
我們在 Profile 頁面中添加一個登出按鈕,點擊該按鈕會跳轉到登錄頁面。
// profile.dart
class Profile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Profile'),
),
body: new Center(
child: new RaisedButton(
child: new Text('Log Out'),
onPressed: () {
Navigator.of(context).pushNamed('/login');
},
),
),
);
}
}
// main.dart
import './home.dart';
import './login.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demos',
routes: {
'/': (BuildContext context) => new Home(),
'/login': (BuildContext context) => new Login()
},
);
}
}
最后效果如下:
至此,Flutter 路由和導航器的內容就總結完畢,接下來,學習 Flutter 中如何進行布局。