--- title: 'Flutter Navigator' tags: Flutter disqus: hackmd --- <style> .red { color: red; } .blue { color: blue; } </style> # Flutter Navigator **目錄:** [TOC] # Navigator 1.0 跳轉至SecondScreen() ```dart= Navigator.push( context, MaterialPageRoute(builder: (context) => const SecondScreen()), ); ``` 返回頁面 ```dart= Navigator.pop(context); ``` ## 傳遞資料 ### A頁傳至B頁 傳值 ```dart= Navigator.push( context, MaterialPageRoute(builder: (context) => const SecondScreen(str: 'FirstPage傳過來的值',)), ); ``` SecondScreen初始化資料 ```dart= class SecondScreen extends StatefulWidget { final String str; const SecondScreen({super.key, required this.str}); @override State<SecondScreen> createState() => _SecondScreenState(); } ``` 使用 ```dart= Text(widget.str) ``` ### B頁傳至A頁 從首頁跳轉時,加入非同步 async、await 處理,等B頁返回資料,會將資料存進 result 變數裡面。 ```dart= String? result; void goToBPage(BuildContext context) async { result = await Navigator.push( context, MaterialPageRoute( builder: (context) => const SecondScreen( str: 'FirstPage傳過來的值', ), ), ); // 沒有呼叫setState的話,並不會更新畫面 setState(() { }); } ``` 透過 Navigator.pop 方法,將要返回資料,放在 result 欄位,將資料送回去首頁。 ```dart= Navigator.pop(context, '123'); ``` 使用 ```dart= Text('返回值: $result') ``` ### 其他方法 每一次跳頁就是堆積木一樣。 假設現在有個情境 A 畫面要跳到 B,在這邊我會寫成 A->B 的樣子。 - push :A->B,積木由上到下,從 A 變成 B,A - pop :A->B,積木由上到下,從 A,B 變成 B - pushReplacement:A->B,蓋上去取代,積木由上到下,從 A 變成 B - popAndPushNamed:A->B,先移開最上層的在蓋上去,積木由上到下,從 A,C 變成 C 再變成 B,C - pushAndRemoveUntil::A->B,先蓋上去然後把底下的全部移開,積木由上到下,從 A,C 變成 B,A,C 再變成 B - 當然不只這些,剩下的可以去 官網 找 ## Using named routes(命名路由) 具有簡單導航和深度鏈接需求的應用程序可以使用 Navigator用於導航和MaterialApp.routes用於深度鏈接的參數: ```dart= @override Widget build(BuildContext context) { return MaterialApp( routes: { '/': (context) => HomeScreen(), '/details': (context) => DetailScreen(), }, ); } ``` ### routes - routes是一個靜態的Map,用於定義所有可能的路由名稱和對應的WidgetBuilder。 - 這些路由在應用程序啟動時就已經被設定好了,並且是固定的,無法根據運行時的情況動態更改。 - 當你使用Navigator.pushNamed時,系統會根據routes中的配置找到對應的WidgetBuilder,然後創建一個新的頁面。 ### onGenerateRoute - onGenerateRoute是一個動態的回調函數,當尋找路由時會被調用。 - 通常用於處理在routes中未定義的動態路由,例如從外部鏈接或其他來源接收的路由。 - 你可以根據settings中的信息返回對應的PageRoute,或者返回null表示找不到對應的路由。 - 可以實現更多的靈活性,例如從網絡獲取路由信息。 總的來說,routes是一種靜態的方式,在應用啟動時就定義了所有的路由名稱和頁面對應關係,而onGenerateRoute則是一種動態的方式,在導航時根據路由名稱動態生成頁面。如果你的應用中的路由比較固定,你可以選擇使用routes來定義它們,如果需要更多的動態性,則可以使用onGenerateRoute。 ### 範例 ```dart= abstract class Routes { static const first = '/'; static const second = '/Second'; static const three = '/Three'; } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return ChangeNotifierProvider<JumpProviderModel>( create: (context) => JumpProviderModel(), child: MaterialApp( onGenerateRoute: (settings) { final uri = Uri.parse(settings.name ?? ""); switch (uri.path) { case Routes.second: final queryParameters = uri.queryParameters; final str = queryParameters['str'] ?? 'Default String'; return MaterialPageRoute( settings: const RouteSettings(name: Routes.second), builder: (context) => SecondScreen(str: str), ); } return null; }, routes: { Routes.first: (context) => const FirstScreen(), Routes.three: (context) => const ThreeScreen(), }, title: 'navigator_v1 Example', initialRoute: Routes.first, ), ); } } ``` 推頁: ```dart= Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: Routes.second), builder: (context) => const SecondScreen( str: 'FirstPage傳過來的值', ), ), ); ``` #### 限制: 儘管命名路由可以處理深層連結,但行為始終保持不變,無法進行自定義調整。當平台收到新的深層連結時,Flutter會將新的路由推送到導航器,無論用戶當前位於何處。 同時,Flutter對於使用命名路由的應用程序<span class="red">不支持瀏覽器的前進按鈕</span>。出於這些原因,我們不建議在大多數應用程序中使用命名路由。 # Navigator 2.0 ## 為什麼需要新的API 在探究具體細節之前,我們有必要了解一下Flutter 團隊為什麼要不惜這些代價對Navigator API 做這次的重構,主要有如下幾點原因。 - **原始API 中的<span class="blue">initialRoute</span>參數,即係統默認的初始頁面,在應用運行後就不能再更改了**。這種情況下,如果用戶接收到一個系統通知,點擊後想要從當前的路由棧狀態[Main -> Profile -> Settings] 重啟切換到新的[Main -> List -> Detail[id=24]路由棧,舊的Navigator API 並沒有一種優雅的實現方式實現這種效果。 - **原始的命令式Navigator API 只提供給了開發者一些非常針對性的接口,如push()、pop()等,而沒有給出一種更靈活的方式讓我們直接操作路由棧**。這種做法其實與Flutter 理念相違背,試想如果我們想要改變某個widget 的所有子組件只需要重建所有子組件並且創建一系列新的widget 即可,而將此概念應用在路由中,當應用中存在一系列路由頁面並想要更改時,我們只能調用push()、pop()這類接口來回操作, 這樣的Flutter 食之無味。 - **嵌套路由下,手機設備自帶的回退按鈕只能由根Navigator 響應**。在目前的應用中,我們很多場景都需要在某個子tab 內單獨管理一個子路由棧。假設有這個場景,用戶在子路由棧中做一系列路由操作之後,點擊系統回退按鈕,消失的將是整個上層的根路由,我們當然可以使用某種措施來避免這種狀況,但歸咎起來,這也不應該是應用開發者應該考慮的問題。 ## Navigator 2.0 Navigator 2.0 新增的聲明式API 主要包含 Page API、Router API 兩個部分,它們各自強大的功能為Navigator 2.0 提供了強有力的基石,本節我就帶讀者們看看它們各自的實現細節。 ### Page Page 是Navigator 2.0 中最常見的類之一,從名字就能知道它的含義就是“**頁面**”,正如widget 就是**組件**一樣,但Page 與Widget 的關係也更加微妙。 與Flutter 中Widget、Element、 RenderObject 三棵樹的概念保持一致。Widget 只保存組件配置信息,框架層內置了一個<span class="blue">createElement()</span>可以創建與之對應的Element 實例。Page 同樣只保存頁面路由相關信息,框架層也存在一個<span class="blue">createRoute()</span>方法可以創建與之對應的Route 實例。 ![](https://hackmd.io/_uploads/Sy2FKVian.png) Widget 和Page 中也都有一個canUpdate()方法,幫助Flutter 判斷其是否已更新或改變: ```dart= // Page bool canUpdate(Page<dynamic> other) { return other.runtimeType == runtimeType && other.key == key; } // Widget static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } ``` 而在代碼層面,Page 類就繼承自我們在舊的Navigator API 用過的RouteSettings: ```dart= abstract class Page<T> extends RouteSettings ``` 其中就保存了包含路由名稱(name,如“/settings”)和路由參數(arguments) 等信息。 #### pages 參數 在新的Navigator 組件中,新增了一個pages參數,它接受的就是一個Page 對象列表,如下這段代碼: ```dart= final List<Page> _pages = []; ``` 此時,運行應用,**Flutter 就會根據這裡pages 列表中的所有Page 對像在底層的路由棧生成對應的Route 實例**。 應用打開某個頁面,就表示在pages 中添加一個Page 對象,系統接收到上層的pages 改變的通知後就會**比較新的pages 與舊的pages**,根據比較結果,Flutter 就會在底層路由棧中新生成一個Route 實例,這樣一個新的頁面就算打開成功了。 ```dart= void addPage(MyPage page) { setState(() => pages.add(page)); } ``` Navigator 組件同樣也新增了一個**onPopPage**參數,接受一個回調函數來響應頁面的pop 事件,如下面代碼中的_onPopPage函數: ```dart= class _MyAppState extends State<MyApp> { bool _onPopPage(Route<dynamic> route, dynamic result) { setState(() => pages.remove(route.settings)); return route.didPop(result); } @override Widget build(BuildContext context) { print('build: $pages'); return // ... Navigator( key: _navigatorKey, onPopPage: _onPopPage, pages: List.of(pages), ) } } ``` 當我們調用<span class='blue'>Navigator.pop()</span>關閉某個頁面時,即能觸發這個函數調用,而函數接受到的route 對象參數就表示需要在pages 中被移除的頁面,在這裡,我們順勢更新pages 列表做移除操作即可。 在<span class='blue'>_onPopPage中</span>,如果我們同意關閉該頁面,則調用<span class='blue'>route.didPop(result)</span>,該函數默認返回true。 當然,我們也完全可以選擇在接收到通知時不更新pages 列表,這完全由我們控制,如下這段代碼: ```dart= @override Widget build(BuildContext context) { debugPrint("RouterDelegate build $_pages"); return Navigator( key: navigatorKey, onPopPage: _onPopPage, pages: List.of(_pages), ); } bool _onPopPage(Route<dynamic> route, result) { // return false; debugPrint("onPopPage"); final didPop = route.didPop(result); if (!didPop) { return false; } if (_pages.length > 1) { _pages.removeLast(); notifyListeners(); return true; } else { return false; } } ``` ### 生命週期 ![](https://hackmd.io/_uploads/r1hdYVoa3.png) ### Using the Router 具有高級導航和路由需求的Flutter應用程序(例如使用直接連結到每個屏幕的Web應用程序,或者具有多個Navigator小部件的應用程序)應該使用路由套件,如go_router,它可以解析路由路徑並在應用程序接收到新的深層連結時配置導航器。 要使用Router,請切換到MaterialApp或CupertinoApp上的router構造函數,並為其提供一個Router配置。常見的路由套件,例如go_router,通常會為您提供配置。例如: ```dart= MaterialApp.router( routerConfig: GoRouter( // … ) ); ``` 因為像 go_router 這樣的包是聲明性的,所以當收到deep link時,它們將始終顯示相同的屏幕。 >如果您不想使用路由包並且希望完全控制應用程序中的導航和路由,請覆蓋 <span class="blue">RouteInformationParser</span>和<span class="blue">RouterDelegate</span>。Page當應用程序中的狀態發生變化時,您可以通過使用參數提供對象列表來精確控制屏幕堆棧 Navigator.pages。有關更多詳細信息,請參閱 RouterAPI 文檔。 ### Using Router and Navigator together 路由器和導航器旨在協同工作。您可以使用 Router API 通過聲明性路由包(例如 go_router)進行導航,或者通過在導航器上調用命令式方法(例如 push() 和 pop())。 當您使用 Router 或聲明性路由包進行導航時,Navigator 上的每個路由都是受頁面支持的,這意味著它是使用 Navigator 構造函數上的 pages 參數從 Page 創建的。相反,任何通過調用 Navigator.push 或 showDialog 創建的 Route 都會向 Navigator 添加無頁路由。 當從導航器中刪除支持分頁的路由時,它後面的所有無頁路由也會被刪除。例如,如果深層鏈接通過從導航器中刪除頁面支持的路由來進行導航,則之後的所有無頁面_路由(直到下一個_頁面支持的路由)也將被刪除。 >無頁面的路由是指在導航過程中被添加到 Navigator 中的路由,但是它們沒有與具體的頁面或 Widget 相關聯。換句話說,這些路由不是通過在 Navigator 構造函數的 pages 參數中添加的,而是通過命令式的方法,例如 Navigator.push() 或 showDialog() 添加的。這樣的路由不會與具體的畫面內容相關聯,而只是表示導航操作的一個步驟。 > >舉個例子,如果你使用 Navigator.push() 方法來在應用程序中打開一個新的畫面,那麼所創建的路由就是一個無頁面的路由。這種情況下,該路由僅表示當前畫面的導航狀態,而不涉及特定的畫面內容。無頁面的路由在一些情況下可能用於導航操作,但它們不會在底層的路由棧中創建具體的畫面實例。 ### Web support 每當使用 Router 導航時,會在瀏覽器的歷史堆疊中添加一個 History API 項目。按下後退按鈕會使用反向時間順序導航,這意味著用戶會返回到先前使用 Router 顯示的先前訪問的位置。這意味著,如果用戶從導航器中彈出一個頁面,然後按下瀏覽器的後退按鈕,先前的頁面會被推回到堆疊中。簡單來說,當使用 Router 導航時,瀏覽器的歷史記錄會隨之更新,以便在按下後退按鈕時正確處理導航的狀態。 # 參考連結 https://www.jianshu.com/p/3472d016a2b8 https://docs.flutter.dev/ui/navigation