Navigator 與 Route


到目前為止,範例都只有一個頁面,對於有著一定功能性的 App 來說,往往不會只有一個頁面,如果你希望有多個頁面,就目前學過的東西來說,一個直覺的想法就是,將 Widget 樹中代表各頁面的 child 置換掉,或者是用 Stack 之類的排版來實現。

基本上這行得通,只不過會有許多細節必須得親自處理,像是被換掉的頁面該怎麼管理?頁面出現的順序?頁面狀態的維護?頁面與頁面之間的訊息溝通等。

在 Flutter 中,涉及頁面間轉換的職責,是由 Navigator 處理,它使用堆疊來管理 Route,你可以將 Route 實例置入(push)Navigator 的堆疊中,或者將堆疊中的 Route 彈出(pop)。

Route 是什麼呢?如方才談到,技術上來說,RouteNavigator 可在堆疊中管理的實例,抽象層面上而言,會讓人聯想到許多 Web 框架中路由表之類的東西,就這方面而言,「路由代表某個資源的銜接」的概念是類似的。

雖然〈Navigate to a new screen and back〉中談到:

In Flutter, screens and pages are called routes. The remainder of this recipe refers to routes.

確實地,在 Flutter 上 Route 銜接的資源,通常是某個頁面,不過 Route 可不是 Widget,舉個例子好了,稍後會用到的 MaterialPageRoute,被置入 Navigator 的堆疊後,最後雖然會使用 builder 指定的 Widgetbuild 出可全螢幕呈現的畫面元件,不過 MaterialPageRoute 銜接的資源除了畫面元件之外,還包含了原生平台相應的轉場動畫的效果。

來看個實例,如果想透過 NavigatorMaterialPageRoute,在 Android 呈現出底下的換頁效果:

Navigator 與 Route

圖片使用的部份,可參考〈Assets 管理〉,先來看範例程式碼:

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('主畫面'),
      ),
      body: GestureDetector(
        onTap: () {
          Navigator.push(context,
            MaterialPageRoute(builder: (_) => DetailPage())
          );
        },
        child: Image.asset('images/caterpillar.png'),
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTap: () => Navigator.pop(context),
        child: Center(
          child: Image.asset('images/caterpillar.png'),
        ),
      ),
    );
  }
}

範例中使用了 GestureDetector,這是個可用來處理子元件發生觸控手勢相關事件,tap 事件就是用手指點一下(類似按鈕的 click),在主頁面圖片的 tap 事件發生時,Navigator.push 將指定的 Route 置入堆疊,也可以使用 Navigator.of(context).push(MaterialPageRoute(builder: (_) => DetailPage())),因為 push 的原始碼是:

static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
  return Navigator.of(context).push(route);
}

簡單來說,一開始堆疊是這樣的:

Navigator 與 Route

Navigator 的堆疊中最底部的路由,代表的資源是 MaterialApphome 指定的 Widget,在 tap 事件發生時,MaterialPageRoute 被置入堆疊:

Navigator 與 Route

在堆疊頂端的路由,代表著會用於螢幕顯示的相關資源,就 MaterialPageRoute 來說,builder 指定的函式,會用來建立最後要的顯示的畫面,在範例中指定的是 DetailPage,因此在轉場動畫之後,看到的就是只有置中顯示的圖片了,這時若又按下圖片,Navigator.pop 會將 MaterialPageRoute 從堆疊中彈出,你也可以寫 Navigator.of(context).pop(),因為原始碼 Navigator.pop 是:

static void pop<T extends Object>(BuildContext context, [ T result ]) {
  Navigator.of(context).pop<T>(result);
}

其中 result 是用來傳遞頁面結果時使用,之後會談到。在彈出 MaterialPageRoute 後,堆疊中就只剩代表 MainPageRoute 實例,也就相當於回到主畫面了。