Flutter-導航與路由堆疊詳解

語言: CN / TW / HK

一個App是由很多page(頁面)組成的,我們需要點選跳轉到不同的頁面,而不是隻是單純通過BottomNavigationBarItem點選設定IndexedStack的屬性進行切換頁面。通常我們會通過路由來統一的管理跳轉。

什麼是路由

存在一個路由對映表,通過唯一的標識找到要跳轉的頁面。在Flutter中,路由管理主要有兩個類:RouteNavigator

Route

一個頁面要想被路由統一管理,必須包裝為一個 Route。MaterialPageRoutePageRoute的子類,表示一個模態路由頁面,還定義了路由構建及切換時過渡動畫的相關介面及屬性。MaterialPageRoute 是 Material 元件庫提供的元件,它可以針對不同平臺,實現與平臺頁面切換動畫風格一致的路由切換動畫。 - 在 iOS 平臺,開啟一個頁面會從右邊滑動到螢幕左邊,返回時會從左邊到右邊消失 - 在 Android 平臺,開啟一個頁面會從螢幕底部滑動到螢幕的頂部,關閉頁面時從頂部滑動到底部消失

繼承關係是這樣的

MaterialPageRoute->PageRoute->ModalRoute->TransitionRoute->OverlayRoute->Route

Navigator

Navigator:管理所有的 Route 的 Widget,通過棧來進行管理的,通常當前螢幕顯示的頁面就是棧頂的路由。

元件路由

路由跳轉,傳入一個路由物件route。該方法是把新的路由新增到Navigator管理的路由物件的棧頂

Navigator.of(context).push(route)

返回上個頁面,這裡返回時可以帶引數的[ T? result ],退出則是從棧頂把路由物件移出

Navigator.of(context).pop()

示例程式碼

child: ElevatedButton( onPressed: (){ Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context){ return PageDetail(); })); }, child: Text('進入詳情頁面'), ),

child: ElevatedButton( onPressed: (){ Navigator.of(context).pop(); }, child: Text('返回'), ),

導航返回攔截

為了避免使用者誤觸返回按鈕而導致 App 退出,在很多 App 中都攔截了使用者點選返回鍵的按鈕,然後進行一些防誤觸判斷,比如當用戶在某一個時間段內點選兩次時,才會認為使用者是要退出(而非誤觸)。Flutter 中可以通過WillPopScope來實現返回按鈕攔截,我們看看 WillPopScope 的預設建構函式:

const WillPopScope({ ... required WillPopCallback onWillPop, required Widget child }) onWillPop 是一個回撥函式,當用戶點選返回按鈕時被呼叫(包括導航返回按鈕及 Android 物理返回按鈕)。該回調需要返回一個Future物件,如果返回的 Future 最終值為false時,則當前路由不出棧(不會返回);最終值為true時,當前路由出棧退出。我們需要提供這個回撥來決定是否退出。

``` @override Widget build(BuildContext context) { WillPopScope( onWillPop: onWillPop, child: Scaffold(

  )

)

onWillPop:() { print('即將退出本介面'); //return Future.value(false);//阻止退出 return Future.value(true); //退出 } } ```

命名路由

基本路由使用還是比較簡單的,但是對於頁面比較多的場景就不太適應用了,每次跳轉新頁面就要建立新路由,這樣就是比較混亂了。而命名路由是給每個頁面起了一個別名,通過這個別名就可以找到開啟這個頁面,這樣管理起來就比較清晰方便了。

要想通過別名來實現頁面切換,就需要用到 MaterialApp 提供的一個頁面名稱對映規則,也就是路由表。這個路由表是一個Map結構,key是頁面的別名,value就是對應頁面

Navigator 使用的4個關鍵屬性

  • initialRoute: 初始路由的,也就是進入APP,預設頁面
  • onGenerateRoute: 路由攔截器,當需要對某個路由做特殊處理時可以使用這個
  • onUnknownRoute: 找不到頁面,預設建立一個錯誤頁面
  • routes:也就在執行路由跳轉的時候,會到路由集合裡面的子路由進行匹配,如果匹配 到那麼就調整到指定頁面

示例程式碼

return MaterialApp( //路由表 routes: { '/': (context) => HomePage(), 'basic': (context) => BasicPage(), 'form_main': (context) => FormMainPage(), 'nav': (context) => NavigationMainPage(), }, //初始路由頁面 initialRoute: 'hoemPage', //找不到路由的頁面 onUnknownRoute: (RouteSettings routerName) { print("未匹配到路由:$routerName.name"); return new MaterialPageRoute(builder: (context) { return ErrorPage(); }); } }

路由鉤子

MaterialApp有一個onGenerateRoute屬性,當呼叫Navigator.pushNamed(…)開啟命名路由時,如果指定的路由名在路由表中已註冊,則會呼叫路由表中的builder函式來生成路由元件;如果路由表中沒有註冊,才會呼叫onGenerateRoute來生成路由。然後可以在路由中通過引數RouteSettings來判斷是否需要特殊處理,比如可以把需要許可權判斷跳轉的路由放到這裡開啟。

onGenerateRoute: (settings) { if (settings.name == "login") { return MaterialPageRoute( builder: (context) { return LoginPage(settings.arguments); } ); } return null; },

Navigator 如何傳參和回傳值

  1. 註冊好之後就可以通過 Navigator.pushNamed() 開啟頁面了

Navigator.pushNamed(context, "basic"); //帶引數的跳轉 Navigator.pushNamed(context, "basic", arguments: '要傳的引數'); 2. 獲取傳遞的引數 Widget build(BuildContext context) { // 1.獲取資料 final args= ModalRoute.of(context)!.settings.arguments; } 3. 返回頁面是通過 Navigator.pop

Navigator.pop(context); Navigator.pop(context, "返回值");

  1. 接收返回的資料,需要修改下跳轉方法 Navigator.pushNamed(context, 'basic').then((value) => print('接收到的資料${value}'));
  2. 返回到指定的頁面是通過 Navigator.popUntil

Navigator.popUntil(context, ModalRoute.withName('路由'));

在平常使用過程中,是可以把RoutesonGenerateRoute單獨提取出來成為一個類,這樣後期維護就比較清晰和修改方便。

命名路由的最重要作用,就是建立了字串識別符號與各個頁面之間的對映關係,使得各個頁面之間完全解耦,應用內頁面的切換隻需要通過一個字串識別符號就可以搞定,為後期模組化打好基礎。

Navigator的各種跳轉方式詳解

  • pushAndRemoveUntil:跳轉到新的頁面,並且刪除新頁面和底部頁面之間所有頁面
  • pushReplacement:新的頁面替換當前頁面,只需要建立新的頁面,當前頁面銷燬
  • pushReplacementNamed:新的頁面替換當前頁面,只是路由的傳遞,命名路由方式,當前頁面銷燬
  • popUntil:返回到指定頁面,其他頁面銷燬
  • popAndPushNamed:退出當前頁面並從彈出新的頁面
  • canPop:判斷當前頁面能否被彈出棧,棧內只有一個頁面時為false,別的時候為true
  • maybePop:判斷依據就是看當前路由是否處在棧中“最底部”的位置,如果不是就退出

pushReplacementNamed和popAndPushNamed

有 A、B、C 三個頁面,A頁面通過 pushNamed 跳轉到 B,B 通過 pushReplacementNamed 跳轉到 C,點選 C 頁面按鈕執行 pop。點選 C 頁面按鈕直接返回到了 A 頁面,而不是 B 頁面,因為 B 頁面使用 pushReplacementNamed 跳轉,路由堆疊變化:

截圖2021-12-09 下午10.45.24.png

如果 B 頁面跳轉到 C 頁面,使用 popAndPushNamed,popAndPushNamed 路由堆疊和 pushReplacementNamed 是一樣,唯一的區別就是 popAndPushNamed 有 B 頁面退出動畫。

pushNamedAndRemoveUntil

有如下場景,應用程式進入首頁,點選登入進入登入頁面,然後進入註冊頁面或者忘記密碼頁面...,登入成功後進入其他頁面,此時不希望返回到登入相關頁面,此場景可以使用 pushNamedAndRemoveUntil。

有A、B、C、D 四個頁面,A 通過push進入 B 頁面,B 通過push進入 C 頁面,C 通過 pushNamedAndRemoveUntil 進入 D 頁面同時刪除路由堆疊中直到 /B 的路由,C 頁面程式碼:

RaisedButton( child: Text('C 頁面'), onPressed: () { Navigator.of(context).pushNamedAndRemoveUntil('/D', ModalRoute.withName('/B')); }, ),

D 頁面按鈕執行 pop,從 C 到 D 堆疊的變化

截圖2021-12-09 下午10.49.15.png

Navigator.of(context).pushNamedAndRemoveUntil('/D', ModalRoute.withName('/B')); 表示跳轉到 D 頁面,同時刪除D 到 B 直接所有的路由,如果刪除所有路由,只儲存 D

Navigator.of(context).pushNamedAndRemoveUntil('/D', (Route route)=>false);

路由堆疊變化:

截圖2021-12-09 下午10.51.20.png

popUntil

有A、B、C、D 四個頁面,D 頁面通過 popUntil 一直返回到 A 頁面,D 頁面程式碼:

RaisedButton( child: Text('D 頁面'), onPressed: () { Navigator.of(context).popUntil(ModalRoute.withName('/A')); }, )

maybePop 和 canPop

在 A 頁面時路由堆疊中只有 A,呼叫 pop 後,路由堆疊變化

截圖2021-12-09 下午10.53.54.png

此時路由堆疊為空,沒有可顯示的頁面,應用程式將會退出或者黑屏,好的使用者體驗不應如此,此時可以使用 maybePop,maybePop 只在路由堆疊有可彈出路由時才會彈出路由

上面的案例在 A 頁面執行maybePop:

RaisedButton( child: Text('A 頁面'), onPressed: () { Navigator.of(context).maybePop(); }, )

點選後不會出現彈出路由,因為當前路由堆疊中只有 A,在 B頁面執行maybePop,將會返回到 A 頁面。

也可以通過 canPop 判斷當前是否可以 pop:

RaisedButton( child: Text('B 頁面'), onPressed: () { if(Navigator.of(context).canPop()){ Navigator.of(context).pop(); } }, )

三方外掛 fluro

fluro 作為一款優秀的 Flutter 企業級路由框架,fluro的使用比官方提供的路由框架要複雜一些,但是卻非常適合中大型專案。因為它具有層次分明、條理化、方便擴充套件和便於整體管理路由等優點。 fluro 的github連結:https://github.com/lukepighetti/fluro。

參考文章:這篇文章對於路由堆疊講解的很詳細:https://juejin.cn/post/6872861234367217672

監聽路由堆疊的變化:https://juejin.cn/post/6873224319561007111