這裡寫自定義目錄标題
- Flutter命名路由及傳參的深度實踐與解讀
-
- 本次深入實踐的起因
- 跳轉後點傳回黑屏或退出應用的BUG再現
-
- BUG示例代碼:
- 執行pop黑屏或退出應用的原由
- 實作類初始化接收不到參數的原由
- 深度實踐整理後的解讀
-
- 深度實踐後的Flutter命名路由示例代碼
- Flutter命名路由示例用到的路由方法
- MaterialApp路由相關屬性配置要點
- routes清單onGenerateRoute攔截器要點
- 示例不同情況路由棧内容分析
- 我的感悟
Flutter命名路由及傳參的深度實踐與解讀
在寫Flutter應用時,實作頁面間交叉跳轉時,通常多是使用命名路由,這樣更友善。但是經常會遇到某些情況不能達到理想的效果,網羅衆文基本都千篇一律,大多數是源自Flutter中文官網或幾個前期博文的加工再發,沒有針對Flutter的命名路由特性進行深度的分析。本人在近期項目需求中遇到了路由問題,順便對Flutter命名路由做了深度實踐,記錄發文,希望對後來者有所幫助!
.
本次深入實踐的起因
在我們的一個項目中有個頁面暫稱為“Page4”,會有不同的頁面在不同的路由位置跳轉到這個頁面,但是需要在這個頁面點确定按鈕時,跳轉到目前路由的前N個頁面,例如是“Page2”(Page4是經過Page2從首頁一路加載過來的),并刷“Page2”。然後要求在“Page2”執行pop方法應該可以傳回到“Page1”頁面。
期望流程及路由記錄如下:
- 從首頁進入到Page4:Home → Page1 → Page2→ Page3 → Page4
- 在Page4點确定按鈕:Home → Page1 → Page2(新建立的)
- 在Page2點傳回按鈕:Home → Page1
在Flutter的路由方法裡,我們發現Navigator.pushNamedAndRemoveUntil方法攜帶ModalRoute.withName參數,剛好是我們需要的。這個方法攜帶該參數後,是從最近向前删除路由到指定的路由為止(不是清空),然後再建立指定的頁面(新創就等于重新整理了)。然後我們在使用時,發現跳轉沒問題,跳轉後再點傳回出現了黑屏,網羅了一大堆原因均不吻合。後又遇到命名路由傳參問題,經仔細實踐,更深入了解路由的原理,最終一并得以解決。
雖然有其他實作方法可以達到目的,我們不在這裡讨論,在這隻探讨Flutter命名路由的深入實踐過程及得出的結果!
.
跳轉後點傳回黑屏或退出應用的BUG再現
BUG示例代碼:
main.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'routes.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '示範命名路由使用',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Home(),
routes: routes,
onGenerateRoute: onGenerateRoute,
);
}
}
.
home.dart
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
Home({Key key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('命名路由示例'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Flutter 歡迎您!'),
RaisedButton(
child: Text("pushNamed跳page01頁"),
onPressed: () {
Navigator.pushNamed(context, '/page01', arguments: '這是來自Home頁的資料');
},
),
],
),
),
);
}
}
.
page01.dart
import 'package:flutter/material.dart';
class Page01 extends StatefulWidget {
Page01({Key key, this.param}) : super(key: key);
final String param;
@override
_Page01State createState() => _Page01State(msg:this.param);
}
class _Page01State extends State<Page01> {
_Page01State({this.msg});
String msg;
@override
void initState() {
print('類初始化收到:' + (msg ?? '空')); // 控制台輸出msg參數,監測傳值變化
super.initState();
}
@override
Widget build(BuildContext context) {
String args = ModalRoute.of(context).settings.arguments;
print('settings收到:' + (args ?? '空')); // 控制台輸出args參數,監測傳值變化
return Scaffold(
appBar: AppBar(title: Text('頁面 page01')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('類初始化收到:' + (msg ?? '空')),
Text('settings收到:' + (args ?? '空')),
RaisedButton(
child: Text("pushNamed跳page02頁"),
onPressed: () {
Navigator.pushNamed(context, '/page02', arguments: '這是來自page01頁的資料');
},
),
],
),
),
);
}
}
.
page02.dart、page03.dart
與page01.dart頁代碼基本一樣,僅标題和傳遞參數略有差别,這裡不再複制
.
page04.dart
import 'package:flutter/material.dart';
class Page04 extends StatefulWidget {
Page04({Key key, this.param}) : super(key: key);
final String param;
@override
_Page04State createState() => _Page04State(msg:this.param);
}
class _Page04State extends State<Page04> {
_Page04State({this.msg});
String msg;
@override
void initState() {
print('類初始化收到:' + (msg ?? '空')); // 控制台輸出msg參數,監測傳值變化
super.initState();
}
@override
Widget build(BuildContext context) {
String args = ModalRoute.of(context).settings.arguments;
print('settings收到:'+(args ?? '空')); // 控制台輸出args參數,監測傳值變化
return Scaffold(
appBar: AppBar(title: Text('頁面 page04')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('類初始化收到:' + (msg ?? '空')),
Text('settings收到:' + (args ?? '空')),
RaisedButton( // A跳轉按鈕
child: Text("保留路由建立page01頁"),
onPressed: () {
Navigator.pushNamed(context, '/page01', arguments:'pushNamed');
},
),
RaisedButton( // B跳轉按鈕
child: Text("删部分路由後建立page01頁"),
onPressed: () {
Navigator.pushNamedAndRemoveUntil(context, '/page01', ModalRoute.withName('/home'), arguments:'pushNamedAndRemoveUntil');
},
),
],
),
),
);
}
}
.
routes.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';
final routes = {
'/home': (context) => Home(),
'/page01': (context, {arguments}) => Page01(param:arguments),
'/page02': (context, {arguments}) => Page02(param:arguments),
'/page03': (context, {arguments}) => Page03(param:arguments),
'/page04': (context, {arguments}) => Page04(param:arguments),
};
Route<dynamic> onGenerateRoute(RouteSettings settings) {
String routeName = settings.name;
print('目前通路路由名:$routeName');
final Function pageContentBuilder = routes[routeName];
if (pageContentBuilder != null) {
if (settings.arguments != null) {
final Route route = MaterialPageRoute(
builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
);
return route;
} else {
final Route route = MaterialPageRoute(
builder: (context) => pageContentBuilder(context),
);
return route;
}
}
return null;
}
.
執行pop黑屏或退出應用的原由
從首頁home頁面,首次一路點選按鈕過來,在page04目前頁時,路由棧内順序應該是:
[’/home’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’]
但實際上裡面沒有存在 ‘/home’,因為建立 home 頁面時沒有使用命名路由,是以路由棧内的名字是:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’],’/'是路由管理器預設給的初始化名字(具體名字不一定是這個)
在 page04.dart 的 A跳轉按鈕 事件中,我們使用的是 pushNamed 方法,實作的結果與我們預期的是一緻的。也就是之前路由中有一個 page02 頁面,在目前 page04 頁面之後再建立一個 page02(因為要重新整理,是以直接使用了建立),這樣傳回的話需要點5次傳回才到首頁,而期望是傳回2次即到首頁。是以嘗試使用 B跳轉按鈕 的事件。
點 A跳轉按鈕 後,路由棧内是:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’]
在 page04.dart 的 B跳轉按鈕 事件中,我們使用的是 pushNamedAndRemoveUntil 方法,期望清掉路由棧中的 ‘page02’, ‘page03’, ‘page04’,然後建立一個新的 page02。
點 B跳轉按鈕 後期望路由棧内是:
[’/home’, ‘/page01’, ‘/page02’]
但目前代碼結果實際不是,經過實踐以為找不到 ‘/home’ 和 ‘/page01’ ,則把路由棧全清空了,實際是:
[’/page02’]
也就是隻有 page02 頁面了,沒有上一頁了,是以這是調用pop方法傳回上一頁,找不到 page01了,這是黑屏或退出現象的主因。
.
實作類初始化接收不到參數的原由
經過測試發現,如果使用上述命名路由的方式,雖然在路由Map中,page01-04都配置了參數傳遞,但是相應頁面中的實作類初始化接參都是空的。經過反複實踐,發現原因出在以下幾點:
- 路由Map中的“(context, {arguments}) => Page01(param:arguments)”這種寫法要配合路由攔截器才有效,否則傳遞不過去參數。
-
main.dart 中 MaterialApp 配置了 routes 屬性并指向了 路由Map(routes.dart中的routes),這樣隻要在routes中能找到的頁面名字,就不會觸發路由攔截器,是以參數傳遞也就不可能有效。
.
深度實踐整理後的解讀
.
深度實踐後的Flutter命名路由示例代碼
下面是示例應用截圖:
.
代碼中關鍵位置标有詳細注釋,這均是經過實踐得出的結論,若有錯誤請以官方文檔為準。
main.dart
import 'package:flutter/material.dart';
import 'routes.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '示範命名路由使用',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
//home: Home(),
// 應用初始化時第一個要加載的路由,通常稱為根路由,不使用這個配置會造成查不到根路由頁面的名稱
//initialRoute: '/',
// 命名路由Map,建立頁面是這裡有的就不會觸發onGenerateRoute事件,不使用則直接觸發onGenerateRoute事件
//routes: routes,
// 路由攔截器,産生新路由頁面時,如果在路由清單routes中查不到,則會觸發這個事件
// 是以沒有給routes屬性指派的話,則執行任何一個命名路由相關的建立頁面方法都會觸發本事件
onGenerateRoute: onGenerateRoute,
);
}
}
.
home.dart
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
Home({Key key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('命名路由示例-首頁'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Flutter 歡迎您!'),
RaisedButton(
child: Text("pushNamed建立page01頁"),
onPressed: () {
Navigator.pushNamed(context, '/page01', arguments: '這是來自Home頁的資料');
},
),
],
),
),
);
}
}
.
page01.dart
import 'package:flutter/material.dart';
class Page01 extends StatefulWidget {
// 初始化時接收建立類時傳遞的參數,儲存在param
Page01({Key key, this.param}) : super(key: key);
// 這個一定要是終極類型,因為本類直接繼承自StatefulWidget(被标記為@immutable)
// 為适用命名路由表,這裡隻設一個參數,需要多個可以傳對象或Map
// 名字不是必須為arguments,但是要與路由配置中參數名稱一緻,見routes.dart的'/page01'一行
final String param;
// 為_Page01State類中可以在build外友善擷取param,這裡傳遞this.param給_Page01State的msg
@override
_Page01State createState() => _Page01State(msg:this.param);
}
class _Page01State extends State<Page01> {
// 初始化擷取Page01傳遞過來的param給本類的msg
_Page01State({this.msg});
// 這裡可以不是終極類型
String msg = '空';
@override
void initState() {
// 控制台輸出msg參數,監測傳值變化
print('Page01類初始化收到:$msg');
super.initState();
}
@override
Widget build(BuildContext context) {
// 通過ModalRoute.of(context).settings.arguments擷取的參數,僅在build内可以
String args = ModalRoute.of(context).settings.arguments;
if (args == null) args = '空';
// 控制台輸出args參數,監測傳值變化
print('page01頁settings收到:$args');
return Scaffold(
appBar: AppBar(
// 标記是第幾個頁面,便于測試識别
title: Text('頁面 page01'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 頁面輸出類初始化收到的參數情況,識别更直覺
Text('類初始化收到:$msg'),
// 頁面輸出類在build内使用ModalRoute的settings收到的參數情況,識别更直覺
Text('settings收到:$args'),
RaisedButton(
child: Text("pushNamed建立page02頁"),
onPressed: () {
// 通過pushNamed方法跳轉到page02,并傳參“這是來自page01頁的資料”
Navigator.pushNamed(context, '/page02', arguments: '這是來自page01頁的資料');
},
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
// 通過popUntil方法直接傳回首頁home,不傳參,便于測試
// 此方法指定的頁面名稱必需在目前路由表中存在,否則會黑屏(例如之前有過清空路由操作)
Navigator.popUntil(context, ModalRoute.withName('/'));
},
child: Text('首頁'),
),
);
}
}
page02.dart、page03.dart
與page01.dart代碼基本相同,隻是傳參與描述與頁面名稱相關的更改一下即可,這裡就不複制
.
page04.dart
import 'package:flutter/material.dart';
class Page04 extends StatefulWidget {
// 初始化時接收建立類時傳遞的參數,儲存在param
Page04({Key key, this.param}) : super(key: key);
// 這個一定要是終極類型,因為本類直接繼承自StatefulWidget(被标記為@immutable)
// 為适用命名路由表,這裡隻設一個參數,需要多個可以傳對象或Map
// 名字不是必須為arguments,但是要與路由配置中參數名稱一緻,見routes.dart的'/page01'一行
final String param;
// 為_Page01State類中可以在build外友善擷取param,這裡傳遞this.param給_Page01State的msg
@override
_Page04State createState() => _Page04State(msg:this.param);
}
class _Page04State extends State<Page04> {
// 初始化擷取Page01傳遞過來的param給本類的msg
_Page04State({this.msg});
// 這裡可以不是終極類型
String msg = '空';
@override
void initState() {
// 控制台輸出msg參數,監測傳值變化
print('Page04類初始化收到:$msg');
super.initState();
}
@override
Widget build(BuildContext context) {
// 通過ModalRoute.of(context).settings.arguments擷取的參數,僅在build内可以
String args = ModalRoute.of(context).settings.arguments;
if (args == null) args = '空';
// 控制台輸出args參數,監測傳值變化
print('page02頁settings收到:$args');
return Scaffold(
appBar: AppBar(
// 标記是第幾個頁面,便于測試識别
title: Text('頁面 page04'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 頁面輸出類初始化收到的參數情況,識别更直覺
Text('類初始化收到:$msg'),
// 頁面輸出類在build内使用ModalRoute的settings收到的參數情況,識别更直覺
Text('settings收到:$args'),
RaisedButton(
child: Text("A:保留路由建立page02頁"),
onPressed: () {
// 通過pushNamed方法建立page02,并傳參方法名
// 此方法儲存原路由棧,新建立一個page02頁面,并壓入路由棧
// 執行後,路由棧内應該是['/home', '/page01', '/page02', '/page03', '/page04', '/page02']
Navigator.pushNamed(context, '/page02', arguments:'pushNamed');
},
),
RaisedButton(
child: Text("B:删部分路由後建立page02頁"),
onPressed: () {
// 通過pushNamedAndRemoveUntil方法帶ModalRoute.withName配置建立page02,并傳參方法名
// 此方法将從路由棧頂部(最近)開始逐一彈出(删除),直到遇到'/home'停止删除('/home'不會被删除),再建立page02
// 執行後,路由棧裡就隻剩下了['/home', '/page01', '/page02'],page02的上一頁就是page01,而不是目前頁page04
Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'pushNamedAndRemoveUntil');
},
),
RaisedButton(
child: Text("C:清空路由棧後建立page02頁"),
onPressed: () {
// 通過pushNamedAndRemoveUntil帶(Route<dynamic> route)=>false配置建立page02,并傳參方法名
// 此方法将清空路由棧(彈出/删除所有路由),然後再建立page02
// 執行後,路由棧裡就隻有['/page02'],page02變成了根頁面,沒有上一頁了
Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'pushNamedAndRemoveUntil');
},
),
RaisedButton(
child: Text("D:替換目前路由無轉場效果建立page02頁"),
onPressed: () {
// 通過pushReplacementNamed方法跳轉到page02,并傳參方法名
// 此方法将在原路由棧替換掉目前的路由(page04),然後再建立page02
// 執行後,路由棧裡是['/home', '/page01', '/page02', '/page03', '/page02'],page02的上一頁就是page03,而不是目前頁page04
Navigator.pushReplacementNamed(context, '/page02', arguments:'pushReplacementNamed');
},
),
RaisedButton(
child: Text("E:替換目前路由有轉場效果建立page02頁"),
onPressed: () {
// 通過popAndPushNamed方法跳轉到page02,并傳參方法名
// 此方法與pushReplacementNamed方法最終在路由棧裡的結果相同
// 隻是執行過程中,此方法有轉場效果,而pushReplacementNamed方法沒有
Navigator.popAndPushNamed(context, '/page02', arguments:'pushReplacementNamed');
},
),
RaisedButton(
child: Text("F:删除部分路由回到page02頁"),
onPressed: () {
// 通過popUntil方法傳回到page02,此方法無法傳參,也不觸發路由攔截器
// 此方法與pushNamedAndRemoveUntil方法最終在路由棧裡的結果相同
// 隻是page02不是建立的,不重新整理頁面保持原有狀态,等同于連續的pop;而pushNamedAndRemoveUntil是建立頁面,類似于重新整理了頁面
// 此方法指定的頁面名稱必需在目前路由表中存在,否則會黑屏(例如之前有過清空路由操作)
Navigator.popUntil(context, ModalRoute.withName('/page02'));
},
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
// 通過popUntil方法直接傳回首頁home,不傳參,便于測試
// 此方法指定的頁面名稱必需在目前路由表中存在,否則會黑屏(例如之前有過清空路由操作)
Navigator.popUntil(context, ModalRoute.withName('/'));
},
child: Text('首頁'),
),
);
}
}
routes.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';
final routes = {
// 如果标注'/',而main.dart中又沒有指定home頁,但指定routes為本Map,則應用初始化時自動加載'/'指定的頁面
// 若是main.dart中指定了home頁,指定routes為本Map,這裡就不要有'/',否則會報警告
'/': (context) => Home(),
// 這裡的“arguments”必須是這個名字,否則取不到通過“pushNamed”等方法傳遞的參數
// 這裡的“param”是與對應的類中的接收參數命名對應的,不是一定要使用“arguments”這個名字
// 這裡的參數傳遞僅對使用onGenerateRoute路由攔截器,并在攔截器内寫入參數傳遞的代碼并觸發了攔截器時才有效
// 這裡的參數傳遞是傳遞給類初始化的參數,與ModalRoute.of(context).settings.arguments無關
//
// 若不需要給類初始化傳參,可以寫成 '/page01': (context) => Page01()這樣
// 同樣可以用ModalRoute.of(context).settings.arguments接收“pushNamed”等方法傳遞的參數
'/page01': (context, {arguments}) => Page01(param:arguments),
'/page02': (context, {arguments}) => Page02(param:arguments),
'/page03': (context, {arguments}) => Page03(param:arguments),
'/page04': (context, {arguments}) => Page04(param:arguments),
};
// 路由攔截器,隻有在MaterialApp的routes屬性沒有指派的情況下,才會每次建立頁面都觸發
// 如果指定了routes屬性,那隻有routes中查不到的時候才會觸發
Route<dynamic> onGenerateRoute(RouteSettings settings) {
String routeName = settings.name;
// 控制台輸出目前攔截的路由名稱,友善清晰的了解路由建立流暢
print('目前通路路由名:$routeName');
// 為了避免報錯,将在命名路由清單查不到的都指向首頁(根頁)
if (!routes.containsKey(routeName)) routeName = '/';
// 擷取頁名對應的類
final Function pageContentBuilder = routes[routeName];
if (settings.arguments != null) {
// 如果帶有參數,則調用該類的時候,需要将參數傳遞過去
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
// 這一行是命名路由順暢應用的關鍵,否則新建立的頁面壓入路由棧的将不是自己定義的名字
// 主要是使用 settings.name 的作用,為新建立的頁面命名
settings: settings,
);
} else {
// 如果沒有帶參數,則調用該類的時候,不需要傳遞參數
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context),
settings: settings,
);
}
}
.
Flutter命名路由示例用到的路由方法
-
pushNamed
保留目前路由棧,在命名路由清單中,根據名稱查找對應的頁面實作類并建立該頁,然後壓入路由棧。
可以攜帶參數(非必傳),若要傳參必需以arguments命名,要傳多個參數可封裝成對象或Map(下同)。
調用示例:
Navigator.of(context).pushNamed("/page1", arguments:'這裡是參數');
// 或
Navigator.pushNamed(context, '/page01', arguments:'這裡是參數');
上面示例表示保留目前路由棧建立page01,向page01頁傳遞字元串參數“這裡是參數”五個漢字。
-
pushNamedAndRemoveUntil
根據條件删除目前路由棧中的路由,然後建立新頁。
⑴ 使用 ModalRoute.withName() 參數:
從目前路由棧頂部(最近的)開始彈出(删除)路由,直到指定的頁面為止,保留指定的頁面。
調用示例:
Navigator.of(context).pushNamedAndRemoveUntil('/page02', ModalRoute.withName('/page01'), arguments:'這裡是參數');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'這裡是參數');
上面示例表示從目前頁開始向前依次删除路由直到page01為止,保留page01,然後建立page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。
.
⑵ 使用 (Route route)=>false 參數:
将目前路由棧清空
調用示例:
Navigator.of(context).pushNamedAndRemoveUntil('/page02', (Route<dynamic> route)=>false, arguments:'這裡是參數');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'這裡是參數');
上面示例表示從将目前路由棧中所有路由删除,然後建立page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。
-
pushReplacementNamed
在目前路由棧中删除目前路由,然後建立新頁。也就是使用建立頁的路由替換目前路由。此方法沒有頁面轉場的效果。
調用示例:
Navigator.of(context).pushReplacementNamed('/page02', arguments:'這裡是參數');
// 或
Navigator.pushReplacementNamed(context, '/page02', arguments:'這裡是參數');
上面示例表示将目前路由棧中的目前頁路由替換為建立的page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。沒有轉場效果
-
popAndPushNamed
此方法與pushReplacementNamed在路由棧中最終結果相同,隻是在進入建立頁面時,有轉場效果。
調用示例:
Navigator.of(context).popAndPushNamed('/page02', arguments:'這裡是參數');
// 或
Navigator.popAndPushNamed(context, '/page02', arguments:'這裡是參數');
上面示例表示将目前路由棧中的目前頁路由替換為建立的page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。有轉場效果
-
popUntil
回到之前曾經打開的指定頁,不重建不重新整理頁面,保持原頁面狀态,删除指定頁之後的所有路由,不能攜帶參數。
調用示例:
Navigator.of(context).popUntil(ModalRoute.withName('/page02'));
// 或
Navigator.popUntil(context, ModalRoute.withName('/page02'));
上面示例表示回到曾經打開的page02,删除page02頁之後的所有路由
.
MaterialApp路由相關屬性配置要點
在MaterialApp中與本例相關的屬性有 home、initialRoute、routes、onGenerateRoute 四個。
- home:指定應用啟動後首次加載的頁面,是類名,不是命名路由的頁面名稱。
- initialRoute:同 home,但是指定的是命名路由routes中的頁面名,不是類名。
- routes:命名路由清單,是一個Map類型,頁面名映射該頁對應的實作類名。
- onGenerateRoute:命名路由攔截器,隻有在routes中查不到頁名時才會觸發這個事件。
▲ 在 home 中指定的頁面,該頁在 routes 中的頁面名稱是不能壓入路由棧的。
▲ 因 home 與 initialRoute 作用基本相同,是以不能同時使用,否則會産生沖突。
▲ 隻有配置了 routes 或 啟用了 onGenerateRoute 後,才可以使用 initialRoute,否則無處查找頁面名會産生錯誤。
▲ 若配置了 routes、home 後,routes 清單中有寫 ‘/’ 映射,将會産生沖突,因為命名路由規則中,’/’ 代表的是根頁,也是初始化應用首先要加載的頁面。是以配置了 routes 就盡量不要配置 home。
▲ 即使在 routes 清單中給實作類配置了參數,也是不能給實作類的初始化參數傳遞參數的。必須配置 onGenerateRoute 并在該事件的方法内實作參數傳遞才可以實作,否則隻能在 build 内通過 ModalRoute.of(context).settings.arguments 擷取傳遞的參數。
▲ 若希望每次通過指令路由建立頁面都能走路由攔截器,那定義routes清單後,不要配置routes屬性。由攔截器去加載routes清單,否則在routes清單存在建立頁名的時候,路由攔截器是不會被觸發的。
.
routes清單onGenerateRoute攔截器要點
通常為了友善,都是把這兩個放在一個獨立的檔案内。
先看一下routes清單的定義,粘貼部分代碼如下:
final routes = {
'/': (context) => Home(),
'/page01': (context) => Page01(),
'/page02': (context, {arguments}) => Page02(param:arguments),
};
▲ 上面代碼中 ‘/’ 是應用初始化時,在沒有配置 initialRoute(在MaterialApp中)和 home 時自動加載的頁面,若配置了initialRoute則會加載initialRoute指定的頁面。是以盡量我們把應用的預設加載頁命名為 ‘/’,這更利于代碼閱讀與交流(其他每個頁名前面的 / 也不是必需的)。
▲ 上面代碼中的 {arguments} 和 param:arguments 是為了給頁面實作類(如:Page02)的初始化參數傳遞參數而寫的,arguments 這個名字不能更改,param 必須與 Page02 類中的接收參數名稱一緻。這個配置要最終實作參數傳遞到實作類初始化參數中,必須有 onGenerateRoute 攔截器配合才能實作。
▲ 如果隻需要在 build 内通過 ModalRoute.of(context).settings.arguments 擷取參數,則在這裡不需要配置參數傳遞,像 ‘/’、’/page01’ 那樣寫即可。
再看一下onGenerateRoute攔截器的定義,粘貼示例代碼如下:
Route<dynamic> onGenerateRoute(RouteSettings settings) {
String routeName = settings.name;
if (!routes.containsKey(routeName)) routeName = '/';
final Function pageContentBuilder = routes[routeName];
if (settings.arguments != null) {
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
settings: settings,
);
} else {
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context),
settings: settings,
);
}
示例攔截器中,先擷取路由名稱(因在MaterialApp中沒有配置routes,是以每次建立命名路由頁面都會觸發這裡)給routeName,然後查詢是否存在于routes的key中,如果不在則把routeName的值更改為 ‘/’,後面就會加載根頁。這是為了處理代碼意外,避免名稱錯誤查不到而造成應用停止。
然後使用 routeName 到 routes 擷取映射的實作類給 pageContentBuilder。
settings.arguments 是通過 pushNamed 等命名路由方法傳遞的參數,在這裡通過這個擷取。
接下來判斷 settings.arguments 是否為 null,如果不是 null 說明有傳遞參數,那在 MaterialPageRoute 加載路由頁面時就要給實作類傳參,否則就不需要。
settings: settings 這句是重中之重, 大部分文章中都沒有這句,這句是為了讓通過攔截器建立的新頁面也有路由名稱,主要使用的是 settings.name 屬性。如果新建立的頁面沒有給名字,後續的使用頁面名稱操作路由時,就可能會出現問題。
.
示例不同情況路由棧内容分析
以打開應用後,以從home頁一路點選按鈕到page04為前提,在示例中頁面page04點選不同的按鈕,對路由棧内的情況有如下影響(以數組格式代替垂直表示的棧圖,數組左側為棧底):
-
page04呈現後的路由棧:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’]
-
點選按鈕 A 後的路由棧(使用pushNamed):
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’]
-
點選按鈕 B 後的路由棧(使用pushNamedAndRemoveUntil):
[’/’, ‘/page01’, ‘/page02’]
-
點選按鈕 C 後的路由棧(使用pushNamedAndRemoveUntil):
[’/page02’]
-
點選按鈕 D 後的路由棧(使用pushReplacementNamed):
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’]
-
點選按鈕 C 後的路由棧(使用popAndPushNamed):
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’]
-
點選按鈕 D 後的路由棧(使用popUntil):
[’/’, ‘/page01’, ‘/page02’]
.
我的感悟
雖說天下文章一大抄,但是技術問題還是需要真正的實踐才能知曉真實的結果。很多技術源于國外,國内資料多為業餘翻譯版。當遇到問題很難搞定的時候,别忘記放下資料深入實踐一下,可能得到的結論更直接。更多可以嘗試自行翻譯外文官網的相關片段,或許可以得到不一樣資訊。這裡還是要感謝翻譯外文技術資料的朋友們,辛苦了!
希望本文能給相關的後來者一定的解惑,若對本文内容有異議,請以官方文檔為準。