1. 基本介紹
Route 在 Flutter 里是極其重要的部分性锭,用來(lái)處理頁(yè)面跳轉(zhuǎn)京痢。本文主要普通路由,命名路由篷店,以及自定義路由等。
如果是簡(jiǎn)單的頁(yè)面跳轉(zhuǎn)可以參考Flutter 頁(yè)面創(chuàng)建與跳轉(zhuǎn)臭家。
2. 示例代碼
代碼下載地址疲陕。如果對(duì)你有幫助的話記得給個(gè)關(guān)注,代碼會(huì)根據(jù)我的 Flutter 專題不斷更新钉赁。
3. 基礎(chǔ)功能
- 命名路由 routes
- 路由跳轉(zhuǎn) push蹄殃、pop
- 初始路由 initialRoute
- 路由攔截 onGenerateRoute
4. 命名 Route 詳解
4.1 容器創(chuàng)建
優(yōu)雅的編程,我們創(chuàng)建一個(gè) materialapp.dart 文件你踩。
import 'package:flutter/material.dart';
class FMMaterialAppVC extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
body: AAA(),
),
routes: {
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
);
}
}
class AAA extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('AAA'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊前往BBB'),
onPressed: (){
Navigator.pushNamed(context, '/bbb');
},
),
),
);
}
}
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊前往CCC'),
onPressed: (){
Navigator.pushNamed(context, '/ccc');
},
),
),
);
}
}
class CCC extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('CCC'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊前往DDD'),
onPressed: (){
Navigator.pushNamed(context, '/ddd');
},
),
),
);
}
}
class DDD extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('DDD'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊回到AAA'),
onPressed: (){
Navigator.popUntil(context, (route) => route.isFirst);
},
),
),
);
}
}
我們?yōu)?BBB诅岩,CCC,DDD 進(jìn)行了命名带膜,效果如下吩谦。
4.2 路由跳轉(zhuǎn)方式之 Push 詳解
在上文中,我們使用了 pushNamed 方法膝藕,推到一個(gè)新的頁(yè)面式廷。
class AAA extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('AAA'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊前往BBB'),
onPressed: (){
Navigator.pushNamed(context, '/bbb');
},
),
),
);
}
}
路由 push 其實(shí)還有很多種寫法。
4.2.1 Navigator.pushNamed
無(wú)參數(shù)
Navigator.pushNamed(context, '/bbb');
有參數(shù)
final datas = {"data": ["1","2","3"]};
Navigator.pushNamed(context, '/bbb', arguments: datas);
4.2.2 Navigator.push
無(wú)參數(shù)
Navigator.push(
context,
MaterialPageRoute(
builder: (context){
return BBB();
}
);
有參數(shù)芭挽,name 可以用來(lái)給路由命名滑废,arguments 用來(lái)傳遞參數(shù)
final datas = {"data": ["1","2","3"]};
Navigator.push(
context,
MaterialPageRoute(
builder: (context){
return BBB();
},
settings: RouteSettings(
name: '/bbb',
arguments: datas,
),
),
);
上面代碼意思就是跳轉(zhuǎn)到 BBB() 頁(yè)面蝗肪,并且給 BBB() 命名為 '/bbb',在路由堆棧中蠕趁,讀取到這個(gè) route 時(shí)薛闪,route.setting.name 與該命名相同。
4.2.3 Navigator.of(context).pushNamed
無(wú)參數(shù)
Navigator.of(context).pushNamed('/bbb');
有參數(shù)
final datas = {"data": ["1","2","3"]};
Navigator.of(context).pushNamed('/bbb',arguments: datas);
4.2.4 Navigator.of(context).push
無(wú)參數(shù)
Navigator.of(context).push(
MaterialPageRoute(
builder: (context){
return BBB();
}
),
);
有參數(shù)
final datas = {"data": ["1","2","3"]};
Navigator.of(context).push(
MaterialPageRoute(
builder: (context){
return BBB();
},
settings: RouteSettings(
name: '/bbb',
arguments: datas,
),
),
);
4.3 路由跳轉(zhuǎn)方式之 Push 進(jìn)階
4.2 中是最為常用的頁(yè)面進(jìn)出棧俺陋,但是在某些場(chǎng)合需要有特殊的處理豁延。
注意:寫法大同小異,后續(xù)就不在贅述傳參方式倔韭,均與4.2相同术浪。
4.3.1 pushNamedAndRemoveUntil
例如我們頁(yè)面從AAA->BBB->CCC->DDD,這樣進(jìn)行頁(yè)面寿酌,但是我們希望 CCC 再使用過(guò)后就被銷毀胰苏。使得頁(yè)面層級(jí)變?yōu)?AAA-BBB-DDD。
- Navigator.pushNamedAndRemoveUntil
如下方代碼醇疼,我們?cè)?CCC 中來(lái)做處理硕并。
我們的頁(yè)面層級(jí)為 AAA->BBB->CCC->DDD,我們?cè)?CCC 中 push 到 DDD 頁(yè)面秧荆,然后從棧頂開(kāi)始刪除倔毙,直到這個(gè)route.setting.name == '/bbb',也就是銷毀 BBB 與 DDD 中間所有的頁(yè)面乙濒。
class CCC extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('CCC'),
),
body: _listView(context),
);
}
ListView _listView(BuildContext context){
return ListView(
children: [
ListTile(
title: Text("Navigator.pushNamed"),
onTap: (){
Navigator.pushNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.pushNamedAndRemoveUntil"),
onTap: (){
Navigator.pushNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.of(context).pushNamedAndRemoveUntil"),
onTap: (){
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/ddd');
},
),
ListTile(
title: Text("Navigator.pushNamedAndRemoveUntil - current"),
onTap: (){
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent);
},
),
ListTile(
title: Text("Navigator.pushReplacementNamed"),
onTap: (){
Navigator.pushReplacementNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.of(context).pushReplacementNamed"),
onTap: (){
Navigator.of(context).pushReplacementNamed('/ddd');
},
),
ListTile(
title: Text("Navigator.pushReplacement"),
onTap: (){
Navigator.pushReplacement(context,
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
},
),
ListTile(
title: Text("Navigator.of(context).pushReplacement"),
onTap: (){
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
},
),
ListTile(
title: Text("Navigator.popAndPushNamed"),
onTap: (){
Navigator.popAndPushNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.of(context).popAndPushNamed"),
onTap: (){
Navigator.of(context).popAndPushNamed('/ddd');
},
),
],
);
}
}
如gif圖陕赃,我們push 順序?yàn)?AAA->BBB->CCC->DDD,返回時(shí)為 DDD->BBB->AAA颁股,CCC頁(yè)面銷毀么库。
- Navigator.of(context).pushNamedAndRemoveUntil
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/bbb');
- Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent)
跳轉(zhuǎn)到指定頁(yè)面,并刪除前邊所有頁(yè)面
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent)
// 等價(jià)于下面這行代碼
//Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/ddd');
4.3.2 pushReplacementNamed
例如我們頁(yè)面從AAA->BBB->CCC->DDD甘有,這樣進(jìn)行頁(yè)面诉儒,但是我們希望 CCC 再使用過(guò)后就被銷毀。使得頁(yè)面層級(jí)變?yōu)?AAA-BBB-DDD亏掀。
注意:同4.2.2區(qū)別忱反,假設(shè)我們的頁(yè)面為 AAA->BBB->CCC->DDD->EEE->FFF,我們?cè)?EEE 頁(yè)面跳轉(zhuǎn)到 FFF 并且 removeUntil('/bbb')滤愕,那么我們的頁(yè)面層級(jí)則會(huì)變成 AAA->BBB->FFF温算。而使用 pushReplacementNamed('/ddd'),則會(huì)變成 AAA->BBB->CCC->DDD->FFF该互。
- Navigator.pushReplacementNamed
Navigator.pushReplacementNamed(context, '/ddd');
Navigator.of(context).pushReplacementNamed
Navigator.of(context).pushReplacementNamed('/ddd');
- Navigator.pushReplacement
具體可以參考 4.2 寫法
Navigator.pushReplacement(context,
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
- Navigator.of(context).pushReplacement
具體可以參考 4.2 寫法
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
4.3.3 popAndPushNamed
先 pop 一級(jí)米者,然后在 push 到對(duì)應(yīng)頁(yè)面,效果與4.3.2 相同
- Navigator.popAndPushNamed
Navigator.popAndPushNamed(context, '/ddd');
- Navigator.of(context).popAndPushNamed
Navigator.of(context).popAndPushNamed('/ddd');
4.4 路由跳轉(zhuǎn)方式之 Pop 詳解
Pop 是路由返回方式,與 push 正好相反蔓搞,push 用來(lái)控制頁(yè)面入棧胰丁,而 pop 則控制頁(yè)面出棧。其實(shí)左上角的返回按鈕會(huì)默認(rèn)執(zhí)行一次 pop 方法喂分。
由于 pop 是返回方式锦庸,我們?cè)谧詈笠粋€(gè)頁(yè)面 DDD 中操作。
class DDD extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('DDD'),
),
body: _listView(context),
);
}
ListView _listView(BuildContext context){
return ListView(
children: [
ListTile(
title: Text('Navigator.pop'),
onTap: (){
Navigator.pop(context);
// final data = {"data":["1","2","3"]};
// Navigator.pop(context, data);
},
),
ListTile(
title: Text('Navigator.pop'),
onTap: (){
Navigator.of(context).pop();
},
),
ListTile(
title: Text('Navigator.canPop'),
onTap: (){
bool canpop = Navigator.canPop(context);
if (canpop) Navigator.pop(context);
},
),
ListTile(
title: Text('Navigator.maybePop'),
onTap: (){
Navigator.maybePop(context);
},
),
],
);
}
}
4.4.1 Navigator.pop
無(wú)參數(shù)
Navigator.pop(context);
有參數(shù)
final data = {"data":["1","2","3"]};
Navigator.pop(context, data);
4.4.2 Navigator.of(context).pop
無(wú)參數(shù)
Navigator.of(context).pop();
有參數(shù)
final data = {"data":["1","2","3"]};
Navigator.of(context).pop(data);
4.4.3 Navigator.canPop
判斷是否可以 pop
bool canpop = Navigator.canPop(context);
if (canpop) Navigator.pop(context);
4.4.4 Navigator.of(context).canPop
bool canpop = Navigator.of(context).canPop();
4.4.5 Navigator.maybePop
先判斷是否可以 pop蒲祈,如果可以甘萧,在pop,相當(dāng)于 4.4.3 中代碼
Navigator.maybePop(context);
4.4.6 Navigator.of(context).maybePop
Navigator.of(context).maybePop(context);
4.5 路由跳轉(zhuǎn)方式之 Pop 進(jìn)階
4.4 中是最為常用的頁(yè)面進(jìn)出棧梆掸,但是在某些場(chǎng)合需要有特殊的處理扬卷。pop 進(jìn)階方法比較少,只有 popUntil 與 popAndPushNamed 酸钦,在4.3.3中講過(guò)后者了怪得,這里不重復(fù)敘述。
- 回到棧頂
Navigator.popUntil(context, (route) => route.isFirst);
- 回到指定頁(yè)面
我們使用下方代碼回到 BBB 頁(yè)面
Navigator.popUntil(context, (route) => route.settings.name == '/bbb');
- 頁(yè)面堆棧了解
這里我們自定義一下方法卑硫,查看一下當(dāng)前堆棧的所有 route
Navigator.popUntil(context, (route) => _lookRoutes(route));
bool _lookRoutes(Route route){
print(route);
return route.isFirst;
}
我們來(lái)看一下上述頁(yè)面的所有效果徒恋。
5. 初始路由 initialRoute
我們注釋掉 home 屬性,使用 initialRoute 來(lái)加載頁(yè)面
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/ccc',
routes: {
'/aaa': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
);
}
5.1 initialRoute 使用了未命名的路由
報(bào)錯(cuò)如下欢伏。
A GlobalKey was used multiple times inside one widget's child list.
注意:initialRoute 需要使用 routes 表中的命名過(guò)的路由入挣,否則會(huì)報(bào)錯(cuò)。
5.2 解決方案
a. 更改 initialRoute 為 routes 表里命名過(guò)得路由。
b. 設(shè)置根路由
設(shè)置路由 '/' ,當(dāng)初始路由異常時(shí),會(huì)停留在根路由。
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/ccc',
routes: {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
);
}
- c. 使用未知路由
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/cc',
routes: {
'/aaa': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
6. 未知路由 onUnknownRoute
class FMMaterialAppVC extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
}
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊前往CCC'),
onPressed: (){
Navigator.pushNamed(context, '/cc');
},
),
),
);
}
}
我們?cè)?BBB 頁(yè)面中 push 一個(gè)錯(cuò)誤的 '/cc' 路由噪馏,按照我們 onUnknownRoute 中的設(shè)置,我們返回一個(gè) AAA(),用這個(gè)可以做一個(gè)統(tǒng)一的報(bào)錯(cuò)頁(yè)面佳吞。
7. 路由攔截 onGenerateRoute
class FMMaterialAppVC extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/',
routes: {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
// onGenerateInitialRoutes: (string){
// return [
// MaterialPageRoute(builder: (context) => AAA()),
// ];
// },
onGenerateRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => CCC());
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
}
應(yīng)該有很多小伙伴跟我一樣,onGenerateRoute 不執(zhí)行魔眨,明明寫了 onGenerateRoute 方法媳维,但是卻不響應(yīng)。查了挺多博客遏暴,沒(méi)有相關(guān)描述侄刽,然后去翻了官方文檔和源碼,終于找到問(wèn)題朋凉。
onGenerateRoute 路由攔截不能與命名路由一起使用州丹,否則會(huì)只執(zhí)行命名路由,不在進(jìn)行攔截。下面我們注釋掉命名路由墓毒,然后通過(guò)路由攔截吓揪,自定義一套路由跳轉(zhuǎn),從而實(shí)現(xiàn)命名路由的功能所计。
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
initialRoute: '/',
// routes: {
// '/': (context) => AAA(),
// '/bbb': (context) => BBB(),
// '/ccc': (context) => CCC(),
// '/ddd': (context) => DDD(),
// },
// onGenerateInitialRoutes: (string){
// return [
// MaterialPageRoute(builder: (context) => AAA()),
// ];
// },
onGenerateRoute: (setting){
print(setting);
final isLogin = true;
final routes = {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
};
if (!isLogin) {
return MaterialPageRoute(builder: (context) => AAA());
}
return MaterialPageRoute(builder: routes[setting.name], settings: setting);
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
舉一反三柠辞,在攔截路由中自定義一張路由表。當(dāng)未登錄時(shí)主胧,對(duì)頁(yè)面進(jìn)行攔截叭首,跳轉(zhuǎn)到登錄頁(yè)面,已登錄時(shí)踪栋,正常加載路由表內(nèi)的頁(yè)面焙格,實(shí)現(xiàn)路由攔截功能。
8. 路由傳值
在App中夷都,頁(yè)面之間的通訊和傳值是非常重要的眷唉,這里只單獨(dú)介紹一下使用路由時(shí)的傳值方式,以及反向傳值的方法损肛。
我們?cè)?BBB 頁(yè)面使用 pushNamed 傳值給 CCC厢破,在 CCC 頁(yè)面介紹如何接收路由傳值。然后在 CCC 頁(yè)面使用 pop 帶參數(shù)回來(lái)治拿,在 BBB 頁(yè)面介紹如何接收路由反參摩泪。
8.1 路由正向傳值以及反參接收
BBB 頁(yè)面 push 到 CCC,并等待 CCC 回來(lái)時(shí)的反參劫谅。
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊前往CCC'),
onPressed: (){
var backValueFromDDD = Navigator.pushNamed(context, '/ccc',arguments: {'value': "我是BBB頁(yè)面?zhèn)鬟^(guò)來(lái)的值"});
backValueFromDDD.then((value){
print("CCC 傳回來(lái)反參了见坑,${value}");
});
// Navigator.pushNamedAndRemoveUntil(context, '/ccc', (route) => route.isCurrent);
},
),
),
);
}
}
我們使用 then 屬性來(lái)接收下一個(gè)頁(yè)面 pop 回來(lái)帶的參數(shù)。
8.2 路由接收正向傳值以及反向傳參
我們?cè)?CCC 類中增加以下代碼捏检。
Widget build(BuildContext context) {
// TODO: implement build
var value = ModalRoute.of(context).settings.arguments;
print("BBB 頁(yè)面帶過(guò)來(lái)參數(shù)了荞驴,${value}");
return Scaffold(
appBar: AppBar(
title: Text('CCC'),
),
body: _listView(context),
);
}
ListTile(
title: Text("Navigator.of(context).pop 傳參"),
onTap: (){
Navigator.of(context).pop({"value":"我是CCC頁(yè)面帶回來(lái)的值"});
},
),
8.3 打印效果
8.4 其他寫法
在 BBB 中可以嘗試以下寫法,可以達(dá)到相同效果贯城。
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('點(diǎn)擊前往CCC'),
onPressed: () async {
var backValueFromDDD = await Navigator.pushNamed(context, '/ccc',arguments: {'value': "我是BBB頁(yè)面?zhèn)鬟^(guò)來(lái)的值"});
backValueFromDDD;
print(backValueFromDDD);
// Navigator.pushNamedAndRemoveUntil(context, '/ccc', (route) => route.isCurrent);
},
),
),
);
}
}
9. 技術(shù)小結(jié)
- 路由在App中是一個(gè)非常重要的功能熊楼,我已經(jīng)從事App開(kāi)發(fā)多年了,對(duì)頁(yè)面堆棧以及原理比較熟悉能犯,所以上手起來(lái)比較容易鲫骗。初學(xué)者需要更多耐心來(lái)理解和嘗試路由的各個(gè)方法。
- 路由這一篇章花費(fèi)時(shí)間較多踩晶,踩坑也比較多执泰,為了盡可能的覆蓋到路由的全部用法,本文篇幅也比較長(zhǎng)渡蜻。