# Flutter Navigator 介紹
使用Navigator小部件來管理你的頁面。實際操作前請先閱讀[好用package - provider 介紹](https://hackmd.io/@BzWzq-x9Rb2G4WG03gcyKg/r1KxK-yq5),了解套件的用法。
## Navigator 1.0(命令式)
他就像是stack,我們所加入的元素是採**後進先出(LIFO)**,只有stack頂部的元素是用戶可看見的。

他提供兩個API
* push
```dart
Navigator.push<bool>(
context,
MaterialPageRoute<bool>(
builder: (BuildContext context) => OnboardingScreen()
),
);
```
* pop
```dart
Navigator.pop(context);
```
## Navigator 2.0(declarative)
使切換頁面更為彈性,告訴程式當狀態為x時,渲染y頁,不用再像以往一步一步回到前一個頁面去,也可以避免閃屏的狀況

### 基本概念
#### 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 就會將該狀態分發給子組件,狀態改變導致子組件重建應用最新的狀態。

當用戶點擊某個按鈕就會觸發類似下面這個函數的調用,該函數又會導致狀態改變而重建子組件。
```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`