使用 Navigator


在〈使用具名路由〉中,看到了可以透過 MaterialApproutes 設定路由表,之後在 Navigator.pushNamed 時依指定的名稱(全名符合)來選擇對應的路由。

在更複雜的情況下,你可能需要有彈性的作法,例如希望 Navigator.pushNamed 時指定的名稱可以帶有參數,或者是指定的 arguments 也可以成為路由選擇的依據等。

你可以透過 MaterialApponGenerateRoute 來達成需求,如果指定的話,建構路由時會呼叫指定的函式。例如,將〈使用具名路由〉中第三個範例改寫為:

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'),
        ),
      ),
    );
  }
}

settingsRouteSettings實例,可以從上頭取得 namearguments 特性,在上頭只是用 name 來決定要使用哪個 WidgetBuilder,作為傳回的 MaterialPageRoute 建構之用。

可想而知地,如果你想透過路由名稱 /detail/001 之類來傳遞參數,就是對範例中 settings.name 進行剖析。

MaterialApp 的文件中有談到,MaterialApp 內部的 Navigator 會依以下順序來取得路由:

  1. 如果有設定 home,拿來作為初始路由(預設是 /)。
  2. 否則,使用 routes 設定的路由表,路由表中必須包含初始路由。
  3. 否則,呼叫 onGenerateRoute,它必須傳回 homeroutes 未處理的路由。
  4. 否則,呼叫 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'),
        ),
      ),
    );
  }
}

來看一下效果如何:

使用 Navigator

嗯?不是說 MaterialPageRoute 銜接的畫面會佔滿螢幕嗎?呃 … API 文件也是這麼寫的啦!不過,這只是你透過 MaterialApp 預設的 Navigator 才會有的行為。

嚴格說來,要看你在 Widget 樹的什麼位置使用了 Navigator,該 Navigator 路由設定下切換過去的畫面元件,會成為該節點的 Widget 子樹。