# Flutter Navigator 介紹 使用Navigator小部件來管理你的頁面。實際操作前請先閱讀[好用package - provider 介紹](https://hackmd.io/@BzWzq-x9Rb2G4WG03gcyKg/r1KxK-yq5),了解套件的用法。 ## Navigator 1.0(命令式) 他就像是stack,我們所加入的元素是採**後進先出(LIFO)**,只有stack頂部的元素是用戶可看見的。 ![](https://i.imgur.com/Mykf7q9.png) 他提供兩個API * push ```dart Navigator.push<bool>( context, MaterialPageRoute<bool>( builder: (BuildContext context) => OnboardingScreen() ), ); ``` * pop ```dart Navigator.pop(context); ``` ## Navigator 2.0(declarative) 使切換頁面更為彈性,告訴程式當狀態為x時,渲染y頁,不用再像以往一步一步回到前一個頁面去,也可以避免閃屏的狀況 ![](https://i.imgur.com/MLWVSxA.png) ### 基本概念 #### Pages Flutter 就會根據這裡pages 列表中的所有Page 對像在底層的路由棧生成對應的Route 實例 ##### 添加新頁面 依據各種自訂義判斷決定跳轉至哪一個頁面,例如下方範例:點擊登入按鈕跳轉至教學頁面 點選登入按鈕,login_screen.dart ```dart Provider.of<AppStateManager>(context, listen: false) .login('mockUsername', 'mockPassword'); ``` 觸發models/app_state_manager.dart內判斷登入狀態事件並通知router ``` void login(String username, String password) { _loggedIn = true; notifyListeners(); } ``` navigator/app_router.dart,接到通知進行判斷需要跳轉到哪一個頁面 ```dart @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, onPopPage: _handlePopPage, pages: [ if (!appStateManager.isInitialized) SplashScreen.page(), //未初始化完顯示SplashScreen if (appStateManager.isInitialized && !appStateManager.isLoggedIn) //初始化結束但未登入使用LoginScreen LoginScreen.page(), if (appStateManager.isLoggedIn && !appStateManager.isOnboardingComplete) OnboardingScreen.page(), //已登入但未閱讀教學頁面顯示教學頁面 //依此類推(略... ); } ``` ##### 移除頁面 在教學頁面點擊返回按鍵觸發pop ``` Navigator.pop(context, true); ``` 觸發onPopPage,onPopPage return true 表示關閉頁面,return false 則表示不進行關閉,**某個頁面是否能夠關閉完全由開發者掌控,而不是單純地交給系統的Navigator.pop()。** onPopPage 只響應路由棧頂層頁面的推出,中間頁面的移除不會調用這個回調函數。 ```dart bool _onPopPage(Route<dynamic> route, dynamic result) { if (...) { return false; } //若在教學頁面點擊返回按鍵則進行登出,並依據pages判斷先跳至初始化頁面接著跳入登入頁面 if (route.settings.name == FooderlichPages.onboardingPath) { appStateManager.logout(); } return true; } ``` ##### 注意事項 1. Navigator `pages` 不可以是空的要不然會噴error。 #### Router 它所管理的狀態就是應用的 路由狀態,結合上節中提到的Page 的概念,我們就可以將其中的pages 看做這裡的路由狀態,當我們改變pages 的內容或狀態時, Router 就會將該狀態分發給子組件,狀態改變導致子組件重建應用最新的狀態。 ![](https://i.imgur.com/XtsVQc0.png) 當用戶點擊某個按鈕就會觸發類似下面這個函數的調用,該函數又會導致狀態改變而重建子組件。 ```dart void _pushPage() { MyRouteDelegate.of(context).push('Route$_counter'); } ``` #### RouterDelegate Router 要完成上面所說的功能主要需要通過配置RouterDelegate(路由代理)實現。 ### 程式範例 #### 創建自己的AppStateManager 添加models/app_state_manager.dart。 ```dart class AppStateManager extends ChangeNotifier { //AppState狀態 bool _initialized = false; //每個屬性的getter方法。不能在AppStateManager之外改變這些屬性。 bool get isInitialized => _initialized; //管理AppState需要用到的客製方法 //添加初始化应用程序 void initializeApp() { Timer(const Duration(milliseconds: 2000), () { _initialized = true; notifyListeners(); }); } // TODO: 添加Login方法 // TODO: 添加Logout方法 } ``` #### 使用剛剛定義新的AppStateManager main.dart ```dart class _AppNameState extends State<AppName> { final _appStateManager = AppStateManager(); @override void initState() { _appRouter = AppRouter( appStateManager: _appStateManager, ); super.initState(); } @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider( create: (context) => _appStateManager, ), ], child: //略... ); } } ``` #### 創建自己的Router 添加navigation/app_router.dart 當用戶點擊返回按鈕或觸發系統的返回按鈕事件時,會觸發一個輔助方法,onPopPage。 ```dart class AppRouter extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { @override final GlobalKey<NavigatorState> navigatorKey; //唯一值 final AppStateManager appStateManager; //路由器將監聽應用程序的狀態變化以配置導航器的頁面列表。 AppRouter({ required this.appStateManager, }) : navigatorKey = GlobalKey<NavigatorState>() { //添加狀態監聽器,當狀態改變時,路由器將用一組新的頁面重新配置導航器。 appStateManager.addListener(notifyListeners); } @override void dispose() { appStateManager.removeListener(notifyListeners); super.dispose(); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, //當用戶點擊返回按鈕或觸發系統的返回按鈕事件時,會觸發輔助方法onPopPage。 onPopPage: _handlePopPage, pages: [ // TODO: Add InitScreen // TODO: Add LoginScreen ], ); } bool _handlePopPage(Route<dynamic> route, result) { //檢查當前路由pop是否成功 if (!route.didPop(result)) { return false; } } @override Future<void> setNewRoutePath(configuration) async => null; } ``` #### 定義靜態方法來創建一個MaterialPage 在各頁面定義了一個靜態方法來創建一個MaterialPage,設置適當的唯一標識符。 ```dart static MaterialPage page() { return MaterialPage( name: '/init', key: ValueKey('/init'), child: const InitScreen(), ); } ``` #### 告知AppStateManager ```dart Provider.of<AppStateManager>(context, listen: false).initializeApp(); ``` 範例: 初始化應用程序 ```dart class _InitScreenState extends State<InitScreen> { @override void didChangeDependencies() { super.didChangeDependencies(); Provider.of<AppStateManager>(context, listen: false).initializeApp(); } //略... ``` `initializeApp`為`AppStateManager`自訂義初始化實需呼叫的func。 #### 加入Navigator `pages` 屬性 ```dart @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, onPopPage: _handlePopPage, pages: [ if (!appStateManager.isInitialized) InitScreen.page(), //這邊 ], ); } ``` ## 安著手機自帶返回按鈕 點擊安著手機自帶返回鍵會直接跳出APP,而不是我們所預期的返回APP上一頁,為解決此問題可如下操作: ```dart return MaterialApp( theme: theme, title: 'Fooderlich', home: Router( routerDelegate: _appRouter, backButtonDispatcher: RootBackButtonDispatcher(), //這行 ), ); ``` 使用戶點擊Android系統的返回按鈕時,會觸發onPopPage。 ## 參考資料 1. https://flutter.cn/community/tutorials/understanding-navigator-v2 2. https://ithelp.ithome.com.tw/articles/10263763 ###### tags: `flutter`