在〈使用具名路由〉中,看到了可以透過 MaterialApp
的 routes
設定路由表,之後在 Navigator.pushNamed
時依指定的名稱(全名符合)來選擇對應的路由。
在更複雜的情況下,你可能需要有彈性的作法,例如希望 Navigator.pushNamed
時指定的名稱可以帶有參數,或者是指定的 arguments
也可以成為路由選擇的依據等。
你可以透過 MaterialApp
的 onGenerateRoute
來達成需求,如果指定的話,建構路由時會呼叫指定的函式。例如,將〈使用具名路由〉中第三個範例改寫為:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(
title: 'Openhome',
// 指定 onGenerateRoute
onGenerateRoute: (settings) {
WidgetBuilder builder;
switch(settings.name) {
case '/':
builder = (_) => MainPage();
break;
case '/detail':
builder = (_) => DetailPage();
break;
default:
throw new Exception('路由名稱有誤: ${settings.name}');
}
return new MaterialPageRoute(builder: builder, settings: settings);
},
));
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主畫面'),
),
body: GestureDetector(
onTap: () => Navigator.pushNamed(context, '/detail', arguments: '說明'),
child: Image.asset('images/caterpillar.png'),
),
);
}
}
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 透過 ModalRoute.of(context).settings.arguments 取得 title
title: Text(ModalRoute.of(context).settings.arguments),
),
body: GestureDetector(
onTap: () => Navigator.pop(context, '結果值'),
child: Center(
child: Image.asset('images/caterpillar.png'),
),
),
);
}
}
settings
是 RouteSettings
實例,可以從上頭取得 name
、arguments
特性,在上頭只是用 name
來決定要使用哪個 WidgetBuilder
,作為傳回的 MaterialPageRoute
建構之用。
可想而知地,如果你想透過路由名稱 /detail/001
之類來傳遞參數,就是對範例中 settings.name
進行剖析。
MaterialApp
的文件中有談到,MaterialApp
內部的 Navigator
會依以下順序來取得路由:
- 如果有設定
home
,拿來作為初始路由(預設是/
)。 - 否則,使用
routes
設定的路由表,路由表中必須包含初始路由。 - 否則,呼叫
onGenerateRoute
,它必須傳回home
或routes
未處理的路由。 - 否則,呼叫
onKnownRoute
。
其實 Navigator
是個 Widget
,它是 StatefulWidget
的子類別,Navigator.pushNamed
之類的靜態方法,實際上是透過 Navigator.of
取得了 NavigatorState
,進一步操作對應的 pushNamed
等方法:
static Future<T> pushNamed<T extends Object>(
BuildContext context,
String routeName, {
Object arguments,
}) {
return Navigator.of(context).pushNamed<T>(routeName, arguments: arguments);
}
而 Navigator.of
會使用 context
,取得 Widget
樹中,最接近的父裔節點之狀態物件:
static NavigatorState of(
BuildContext context, {
bool rootNavigator = false,
bool nullOk = false,
}) {
final NavigatorState navigator = rootNavigator
? context.findRootAncestorStateOfType<NavigatorState>()
: context.findAncestorStateOfType<NavigatorState>();
assert(() {
if (navigator == null && !nullOk) {
throw FlutterError(
'Navigator operation requested with a context that does not include a Navigator.\n'
'The context used to push or pop routes from the Navigator must be that of a '
'widget that is a descendant of a Navigator widget.'
);
}
return true;
}());
return navigator;
}
這意謂著 Navigator
可以被安排在 Widget
樹中的任何地方,透過路由管理,於該節點進行更有彈性的頁面切換效果,例如:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(
title: 'Openhome',
home: MainPage()
));
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主畫面'),
),
// 使用 Navigator 管理路由,以便更有彈性地切換頁面
body: Navigator(
onGenerateRoute: (settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/':
builder = (_) => Caterpillar();
break;
case '/detail':
builder = (_) => DetailPage();
break;
default:
throw new Exception('路由名稱有誤: ${settings.name}');
}
return new MaterialPageRoute(builder: builder, settings: settings);
},
),
);
}
}
class Caterpillar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('挖土機?毛毛蟲?'),
),
body: GestureDetector(
onTap: () => Navigator.pushNamed(context, '/detail', arguments: '說明'),
child: Image.asset('images/caterpillar.png')
),
);
}
}
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 透過 ModalRoute.of(context).settings.arguments 取得 title
title: Text(ModalRoute.of(context).settings.arguments),
),
body: GestureDetector(
onTap: () => Navigator.pop(context, '結果值'),
child: Center(
child: Image.asset('images/caterpillar.png'),
),
),
);
}
}
來看一下效果如何:
嗯?不是說 MaterialPageRoute
銜接的畫面會佔滿螢幕嗎?呃 … API 文件也是這麼寫的啦!不過,這只是你透過 MaterialApp
預設的 Navigator
才會有的行為。
嚴格說來,要看你在 Widget
樹的什麼位置使用了 Navigator
,該 Navigator
路由設定下切換過去的畫面元件,會成為該節點的 Widget
子樹。