大部分應(yīng)用程序都包含多個(gè)頁面,并希望用戶能從當(dāng)前屏幕平滑過渡到另一個(gè)屏幕枫疆。移動(dòng)應(yīng)用程序通常通過被稱為“屏幕”或“頁面”的全屏元素來顯示內(nèi)容爵川。在 Flutter 中,這些元素被稱為路由(Route)养铸,它們由導(dǎo)航器(Navigator)控件管理雁芙。導(dǎo)航器管理著路由對(duì)象的堆棧并提供管理堆棧的方法轧膘,如 Navigator.push
和 Navigator.pop
钞螟,通過路由對(duì)象的進(jìn)出棧來使用戶從一個(gè)頁面跳轉(zhuǎn)到另一個(gè)頁面。
查看示例代碼谎碍。
基本用法
Navigator 的基本用法鳞滨,從一個(gè)頁面跳轉(zhuǎn)到另一個(gè)頁面,通過第二頁面上的返回按鈕回到第一個(gè)頁面蟆淀。
創(chuàng)建兩個(gè)頁面
首先創(chuàng)建兩個(gè)頁面拯啦,每個(gè)頁面包含一個(gè)按鈕。點(diǎn)擊第一個(gè)頁面上的按鈕將導(dǎo)航到第二個(gè)頁面熔任。點(diǎn)擊第二個(gè)頁面上的按鈕將返回到第一個(gè)頁面褒链。初始時(shí)顯示第一個(gè)頁面。
// 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,
),
),
);
}
}
跳轉(zhuǎn)到第二頁面
為了導(dǎo)航到新的頁面疑苔,我們需要調(diào)用 Navigator.push 方法甫匹。該方法將添加 Route 到路由棧中!
我們可以直接使用 MaterialPageRoute 創(chuàng)建路由惦费,它是一種模態(tài)路由兵迅,可以通過平臺(tái)自適應(yīng)的過渡效果來切換屏幕。默認(rèn)情況下薪贫,當(dāng)一個(gè)模態(tài)路由被另一個(gè)替換時(shí)恍箭,上一個(gè)路由將保留在內(nèi)存中,如果想釋放所有資源瞧省,可以將 maintainState
設(shè)置為 false
扯夭。
給第一個(gè)頁面上的按鈕添加 onPressed
回調(diào):
onPressed: () {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new SecondScreen()),
);
},
返回第一個(gè)頁面
Scaffold
控件會(huì)自動(dòng)在 AppBar
上添加一個(gè)返回按鈕,點(diǎn)擊該按鈕會(huì)調(diào)用 Navigator.pop
鞍匾。
現(xiàn)在希望點(diǎn)擊第二個(gè)頁面中間的按鈕也能回到第一個(gè)頁面勉抓,添加回調(diào)函數(shù),調(diào)用 Navigator.pop
:
onPressed: () {
Navigator.pop(context);
}
頁面跳轉(zhuǎn)傳值
在進(jìn)行頁面切換時(shí)候学,通常還需要將一些數(shù)據(jù)傳遞給新頁面藕筋,或是從新頁面返回?cái)?shù)據(jù)。考慮此場景:我們有一個(gè)文章列表頁隐圾,點(diǎn)擊每一項(xiàng)會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的內(nèi)容頁伍掀。在內(nèi)容頁中,有喜歡和不喜歡兩個(gè)按鈕暇藏,點(diǎn)擊任意按鈕回到列表頁并顯示結(jié)果蜜笤。
我會(huì)接著上面的例子繼續(xù)編寫。
定義 Article 類
首先我們創(chuàng)建一個(gè) Article 類盐碱,擁有兩個(gè)屬性:標(biāo)題把兔、內(nèi)容。
class Article {
String title;
String content;
Article({this.title, this.content});
}
創(chuàng)建列表頁面和內(nèi)容頁面
列表頁面中初始化 10 篇文章瓮顽,然后使用 ListView
顯示它們县好。
內(nèi)容頁面標(biāo)題顯示文章的標(biāo)題,主體部分顯示內(nèi)容暖混。
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}'),
),
);
}
}
跳轉(zhuǎn)到內(nèi)容頁并傳遞數(shù)據(jù)
接下來缕贡,當(dāng)用戶點(diǎn)擊列表中的文章時(shí)將跳轉(zhuǎn)到ContentScreen
,并將 article 傳遞給 ContentScreen
拣播。
為了實(shí)現(xiàn)這一點(diǎn)晾咪,我們將實(shí)現(xiàn) ListTile
的 onTap 回調(diào)。 在的 onTap 回調(diào)中贮配,再次調(diào)用Navigator.push
方法谍倦。
return new ListTile(
title: new Text(articles[index].title),
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new ContentScreen(articles[index]),
),
);
},
);
內(nèi)容頁返回?cái)?shù)據(jù)
在內(nèi)容頁底部添加兩個(gè)按鈕,點(diǎn)擊按鈕時(shí)跳轉(zhuǎn)會(huì)列表頁面并傳遞參數(shù)泪勒。
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
列表項(xiàng)的 onTap
回調(diào)昼蛀,處理內(nèi)容頁面返回的數(shù)據(jù)并顯示。
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),
),
);
}
},
定制路由
通常酣藻,我們可能需要定制路由以實(shí)現(xiàn)自定義的過渡效果等曹洽。定制路由有兩種方式:
- 繼承路由子類,如:PopupRoute辽剧、ModalRoute 等送淆。
- 使用 PageRouteBuilder 類通過回調(diào)函數(shù)定義路由。
下面使用 PageRouteBuilder 實(shí)現(xiàn)一個(gè)頁面旋轉(zhuǎn)淡出的效果怕轿。
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),
),
);
}
},
命名導(dǎo)航器路由
通常偷崩,移動(dòng)應(yīng)用管理著大量的路由,并且最容易的是使用名稱來引用它們撞羽。路由名稱通常使用路徑結(jié)構(gòu):“/a/b/c”阐斜,主頁默認(rèn)為 “/”。
創(chuàng)建 MaterialApp
時(shí)可以指定 routes
參數(shù)诀紊,該參數(shù)是一個(gè)映射路由名稱和構(gòu)造器的 Map谒出。MaterialApp
使用此映射為導(dǎo)航器的 onGenerateRoute 回調(diào)參數(shù)提供路由。
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(),
},
);
}
}
路由的跳轉(zhuǎn)時(shí)調(diào)用 Navigator.pushNamed
:
Navigator.of(context).pushNamed('/new');
這里有一個(gè)問題就是使用 Navigator.pushNamed
時(shí)無法直接給新頁面?zhèn)鲄?shù),目前官方還沒有標(biāo)準(zhǔn)解決方案笤喳,我知道的方案是在 onGenerateRoute
回調(diào)中利用 URL 參數(shù)自行處理为居。
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 傳遞參數(shù)
Navigator.of(context).pushNamed('/new/xxx');
嵌套路由
一個(gè) App 中可以有多個(gè)導(dǎo)航器,將一個(gè)導(dǎo)航器嵌套在另一個(gè)導(dǎo)航器下面可以創(chuàng)建一個(gè)內(nèi)部的路由歷史杀狡。例如:App 主頁有底部導(dǎo)航欄蒙畴,每個(gè)對(duì)應(yīng)一個(gè) Navigator,還有與主頁處于同一級(jí)的全屏頁面呜象,如登錄頁面等膳凝。接下來,我們實(shí)現(xiàn)這樣一個(gè)路由結(jié)構(gòu)恭陡。
添加 Home 頁面
添加 Home 頁面蹬音,底部導(dǎo)航欄切換主頁和我的頁面。
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 中有兩個(gè)路由頁面:home 和 demo1祟绊。home 顯示一個(gè)按鈕楼入,點(diǎn)擊按鈕調(diào)轉(zhuǎn)到前面的 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');
},
),
),
);
}
}
效果如下圖:
可以看到,點(diǎn)擊按鈕跳轉(zhuǎn)到 demo1 頁面后嘉熊,底部的 tab 欄并沒有消失遥赚,因?yàn)檫@是在子導(dǎo)航器中進(jìn)行的跳轉(zhuǎn)。要想顯示全屏頁面覆蓋底欄阐肤,我們需要通過根導(dǎo)航器進(jìn)行跳轉(zhuǎn)凫佛,也就是 MaterialApp
內(nèi)部的導(dǎo)航器。
我們?cè)?Profile 頁面中添加一個(gè)登出按鈕孕惜,點(diǎn)擊該按鈕會(huì)跳轉(zhuǎn)到登錄頁面愧薛。
// 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()
},
);
}
}
最后效果如下:
至此,F(xiàn)lutter 路由和導(dǎo)航器的內(nèi)容就總結(jié)完畢衫画,接下來毫炉,學(xué)習(xí) Flutter 中如何進(jìn)行布局。