在〈使用具名路由〉中,看到了可以透過 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 子樹。

