【文章內容使用 Gemini 1.5 Pro 自動產生】
學習 Flutter 的全新導航和路由系統
本文說明 Flutter 的全新 Navigator 和 Router API 如何運作。如果您關注 Flutter 的公開 設計文件,您可能已經看到這些新功能被稱為 Navigator 2.0 和 Router。我們將探討這些 API 如何讓您更精細地控制應用程式中的螢幕,以及如何使用它們來解析路由。
這些新的 API 並不是重大變更,它們只是添加了一個新的 聲明式 API。在 Navigator 2.0 之前,很難推送或彈出多個頁面,或移除當前頁面下方的一個頁面。但是,如果您對當前 Navigator 的運作方式感到滿意,您可以繼續以相同的方式(命令式)使用它。
Router 提供了從底層平台處理路由和顯示適當頁面的能力。在本文中,Router 被配置為解析瀏覽器 URL 以顯示適當的頁面。
本文將幫助您選擇最適合您的應用程式的 Navigator 模式,並說明如何使用 Navigator 2.0 來解析瀏覽器 URL 並完全控制活動頁面堆疊。本文中的練習展示了如何構建一個應用程式,該應用程式可以處理來自平台的進站路由,並管理應用程式的頁面。以下 GIF 顯示了示例應用程式的運作方式:
Navigator 1.0
如果您使用 Flutter,您可能正在使用 Navigator,並且熟悉以下概念:
- Navigator - 一個管理 Route 物件堆疊的 Widget。
- Route - 由 Navigator 管理的一個物件,表示一個螢幕,通常由 MaterialPageRoute 等類別實作。
在 Navigator 2.0 之前,Route 被推送和彈出到 Navigator 的堆疊上,使用 命名路由 或 匿名路由。接下來的幾節將簡要回顧這兩種方法。
匿名路由
大多數行動應用程式會在彼此之上顯示螢幕,就像一個堆疊一樣。在 Flutter 中,使用 Navigator 就可以輕鬆實現這一點。
MaterialApp 和 CupertinoApp 在幕後都已使用 Navigator。您可以使用 Navigator.of() 存取 Navigator,或使用 Navigator.push() 顯示一個新的螢幕,並使用 Navigator.pop() 返回上一個螢幕:
當呼叫 push() 時,DetailScreen Widget 會被放置在 HomeScreen Widget 的頂部,如下所示:
上一個螢幕 (HomeScreen) 仍然是 Widget 樹的一部分,因此與其關聯的任何 State 物件都會在 DetailScreen 可見時保留下來。
命名路由
Flutter 也支援命名路由,這些路由在 MaterialApp 或 CupertinoApp 的 routes 參數中定義:
這些路由必須預先定義。雖然您可以 將參數傳遞給命名路由,但您無法從路由本身解析參數。例如,如果應用程式在網頁上運行,您無法從像 /details/:id 這樣的路由中解析 ID。
使用 onGenerateRoute 的進階命名路由
處理命名路由的更靈活方法是使用 onGenerateRoute。此 API 使您能夠處理所有路徑:
以下是完整的示例:
這裡,settings 是 RouteSettings 的一個實例。name 和 arguments 字段是呼叫 Navigator.pushNamed 時提供的數值,或者設定為 initialRoute 的數值。
Navigator 2.0
Navigator 2.0 API 為框架添加了新的類別,以便使應用程式的螢幕成為應用程式狀態的函數,並提供從底層平台(例如 Web URL)解析路由的能力。以下是新功能的概述:
- Page - 一個不可變的物件,用於設定導航器的歷史堆疊。
- Router - 配置 Navigator 要顯示的頁面列表。通常,這個頁面列表會根據底層平台或應用程式狀態的變化而變化。
- RouteInformationParser,它會從 RouteInformationProvider 中獲取 RouteInformation,並將其解析為一個使用者定義的資料類型。
- RouterDelegate - 定義 Router 如何了解應用程式狀態變化以及如何對它們做出響應的應用程式特定行為。它的工作是監聽 RouteInformationParser 和應用程式狀態,並使用當前的 Page 列表構建 Navigator。
- BackButtonDispatcher - 向 Router 報告後退按鈕按下。
下圖顯示了 RouterDelegate 如何與 Router、RouteInformationParser 和應用程式的狀態交互:
以下是這些部分如何交互的示例:
- 當平台發出一個新的路由(例如,「books/2」)時,RouteInformationParser 會將其轉換為一個抽象資料類型 T,您可以在您的應用程式中定義這個資料類型(例如,一個名為 BooksRoutePath 的類別)。
- RouterDelegate 的 setNewRoutePath 方法會使用這個資料類型被呼叫,並且必須更新應用程式狀態以反映變化(例如,透過設定 selectedBookId)並呼叫 notifyListeners。
- 當呼叫 notifyListeners 時,它會告訴 Router 重新建立 RouterDelegate(使用其 build() 方法)。
- RouterDelegate.build() 返回一個新的 Navigator,其頁面現在反映了應用程式狀態的變化(例如,selectedBookId)。
Navigator 2.0 練習
本節將引導您完成使用 Navigator 2.0 API 的練習。我們將最終獲得一個應用程式,該應用程式可以與 URL 列保持同步,並處理來自應用程式和瀏覽器的後退按鈕按下,如以下 GIF 所示:
若要繼續操作,請 切換到 master channel,使用 Web 支援建立一個新的 Flutter 專案,並將 lib/main.dart
的內容替換為以下內容:
頁面
Navigator 在其建構函數中有一個新的 pages 參數。如果 Page 物件列表發生變化,Navigator 會更新路由堆疊以匹配。若要了解這如何運作,我們將構建一個顯示書籍列表的應用程式。
在 _BooksAppState 中,保留兩個狀態資訊:書籍列表和選中的書籍:
然後在 _BooksAppState 中,返回一個帶有 Page 物件列表的 Navigator:
由於這個應用程式有兩個螢幕,一個是書籍列表,另一個是顯示詳細資訊的螢幕,因此如果選中了書籍,請添加第二個(詳細資訊)頁面(使用 集合中的 if):
請注意,頁面的鍵由書籍物件的數值定義。這告訴 Navigator,當書籍物件不同時,這個 MaterialPage 物件與另一個物件不同。如果沒有唯一的鍵,框架無法確定何時顯示不同 Page 之間的轉場動畫。
注意:如果您願意,您也可以擴展 Page 以自訂行為。例如,此頁面添加了一個自訂轉場動畫:
最後,在沒有提供 onPopPage 回撥的情況下提供 pages 參數是一個錯誤。此函數在呼叫 Navigator.pop() 時被呼叫。它應該用於更新狀態(決定頁面列表的狀態),並且必須呼叫路由上的 didPop 來確定彈出是否成功:
在更新應用程式狀態之前檢查 didPop 是否失敗非常重要。
使用 setState 會通知框架呼叫 build() 方法,該方法在 _selectedBook 為 null 時返回一個帶有單個頁面的列表。
以下是完整的示例:
就目前而言,這個應用程式只允許我們以聲明式的方式定義頁面堆疊。我們無法處理平台的後退按鈕,並且瀏覽器的 URL 在我們導航時不會改變。
Router
到目前為止,應用程式可以顯示不同的頁面,但它無法處理來自底層平台的路由,例如,如果使用者在瀏覽器中更新 URL。
本節將說明如何實作 RouteInformationParser、RouterDelegate 並更新應用程式狀態。設定好後,應用程式將與瀏覽器的 URL 保持同步。
資料類型
RouteInformationParser 會將路由資訊解析為一個使用者定義的資料類型,因此我們將首先定義它:
在這個應用程式中,應用程式中的所有路由都可以使用單個類別來表示。相反,您可能選擇使用實作超類別的不同類別,或以其他方式管理路由資訊。
RouterDelegate
接下來,添加一個擴展 RouterDelegate 的類別:
在 RouterDelegate 上定義的泛型類型是 BookRoutePath,它包含決定要顯示哪些頁面所需的所有狀態。
我們需要將一些邏輯從 _BooksAppState 移動到 BookRouterDelegate,並建立一個 GlobalKey。在這個示例中,應用程式狀態直接儲存在 RouterDelegate 上,但也可以分開到另一個類別中。
為了在 URL 中顯示正確的路徑,我們需要根據應用程式的當前狀態返回一個 BookRoutePath:
接下來,RouterDelegate 中的 build 方法需要返回一個 Navigator:
onPopPage 回撥現在使用 notifyListeners 而不是 setState,因為這個類別現在是 ChangeNotifier,而不是 Widget。當 RouterDelegate 通知其監聽器時,Router Widget 也會被通知 RouterDelegate 的 currentConfiguration 已更改,並且需要再次呼叫其 build 方法以構建一個新的 Navigator。
_handleBookTapped 方法也需要使用 notifyListeners 而不是 setState:
當一個新的路由被推送到應用程式時,Router 會呼叫 setNewRoutePath,這讓我們的應用程式有機會根據路由的變化來更新應用程式狀態:
RouteInformationParser
RouteInformationParser 提供了一個鉤子來解析進站路由 (RouteInformation) 並將其轉換為使用者定義的類型 (BookRoutePath)。使用 Uri 類別來處理解析:
此實作特定於此應用程式,而不是通用的路由解析解決方案。稍後將詳細說明。
若要使用這些新的類別,我們使用新的 MaterialApp.router 建構函數,並傳遞我們自定義的實作:
以下是完整的示例:
在 Chrome 中運行此示例現在會顯示路由的導航方式,並在手動編輯 URL 時導航到正確的頁面。
TransitionDelegate
您可以提供 TransitionDelegate 的自定義實作,它會自訂當頁面列表發生變化時路由在螢幕上出現(或從螢幕中移除)的方式。如果您需要自訂此功能,請繼續閱讀,但如果您對預設行為感到滿意,您可以跳過本節。
為 Navigator 提供一個自訂 TransitionDelegate,它定義了所需的行為:
例如,以下實作會禁用所有轉場動畫:
此自定義實作會覆蓋 resolve(),它負責將各種路由標記為推送、彈出、添加、完成或移除:
- markForPush - 使用動畫轉場顯示路由
- markForAdd - 使用 無 動畫轉場顯示路由
- markForPop - 使用動畫轉場移除路由,並使用結果完成它。在這個上下文中,「完成」意味著結果物件會被傳遞給 AppRouterDelegate 上的 onPopPage 回撥。
- markForComplete - 使用無轉場移除路由,並使用結果完成它
- markForRemove - 使用無動畫轉場移除路由,並且不完成。
此類別只會影響 聲明式 API,這就是為什麼 後退 按鈕仍然顯示轉場動畫的原因。
此示例如何運作: 此示例會查看新的路由和即將離開螢幕的路由。它會遍歷 newPageRouteHistory 中的所有物件,並使用 markForAdd 將它們標記為無轉場動畫地添加。接下來,它會遍歷 locationToExitingPageRoute 映射的數值。如果它找到一個被標記為 isWaitingForExitingDecision 的路由,那麼它會呼叫 markForRemove 來指示該路由應該使用無轉場和無完成的方式移除。
嵌套路由
這個更大的演示展示了如何在另一個 Router 中添加 Router。許多應用程式需要為 BottomAppBar 中的目標設定路由,以及為其上方的視圖堆疊設定路由,這 需要兩個 Navigator。若要做到這一點,應用程式會使用一個應用程式狀態物件來儲存應用程式特定的導航狀態(選中的菜單索引和選中的書籍物件)。此示例還展示了如何配置哪個 Router 處理後退按鈕。
接下來要做什麼
本文探討了如何為特定應用程式使用這些 API,但也可以用於構建一個更高階的 API 套件。我們希望您能加入我們,探索建立在這些功能之上的更高階 API 能夠為使用者做些什麼。
學習 Flutter 的全新導航和路由系統 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。