在上一篇文章中,我帶你一起學習了如何在 Flutter 中實現跨組件數據傳遞。其中,InheritedWidget 適用于子 Widget 跨層共享父 Widget 數據的場景,如果子 Widget 還需要修改父 Widget 數據,則需要和 State 一起配套使用。而 Notification,則適用于父 Widget 監聽子 Widget 事件的場景。對于沒有父子關系的通信雙方,我們還可以使用 EventBus 實現基于訂閱 / 發布模式的機制實現數據交互。
如果說 UI 框架的視圖元素的基本單位是組件,那應用程序的基本單位就是頁面了。對于擁有多個頁面的應用程序而言,如何從一個頁面平滑地過渡到另一個頁面,我們需要有一個統一的機制來管理頁面之間的跳轉,通常被稱為**路由管理或導航管理**。
我們首先需要知道目標頁面對象,在完成目標頁面初始化后,用框架提供的方式打開它。比如,在 Android/iOS 中我們通常會初始化一個 Intent 或 ViewController,通過 startActivity 或 pushViewController 來打開一個新的頁面;而在 React 中,我們使用 navigation 來管理所有頁面,只要知道頁面的名稱,就可以立即導航到這個頁面。
其實,Flutter 的路由管理也借鑒了這兩種設計思路。那么,今天我們就來看看,如何在一個 Flutter 應用中管理不同頁面的命名和過渡。
## 路由管理
在 Flutter 中,頁面之間的跳轉是通過 Route 和 Navigator 來管理的:
* Route 是頁面的抽象,主要負責創建對應的界面,接收參數,響應 Navigator 打開和關閉;
* 而 Navigator 則會維護一個路由棧管理 Route,Route 打開即入棧,Route 關閉即出棧,還可以直接替換棧內的某一個 Route。
而根據是否需要提前注冊頁面標識符,Flutter 中的路由管理可以分為兩種方式:
* 基本路由。無需提前注冊,在頁面切換時需要自己構造頁面實例。
* 命名路由。需要提前注冊頁面標識符,在頁面切換時通過標識符直接打開新的路由。
接下來,我們先一起看看基本路由這種管理方式吧。
### 基本路由
在 Flutter 中,**基本路由的使用方法和 Android/iOS 打開新頁面的方式非常相似**。要導航到一個新的頁面,我們需要創建一個 MaterialPageRoute 的實例,調用 Navigator.push 方法將新頁面壓到堆棧的頂部。
其中,MaterialPageRoute 是一種路由模板,定義了路由創建及切換過渡動畫的相關配置,可以針對不同平臺,實現與平臺頁面切換動畫風格一致的路由切換動畫。
而如果我們想返回上一個頁面,則需要調用 Navigator.pop 方法從堆棧中刪除這個頁面。
下面的代碼演示了基本路由的使用方法:在第一個頁面的按鈕事件中打開第二個頁面,并在第二個頁面的按鈕事件中回退到第一個頁面:
~~~
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 打開頁面
onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 回退頁面
onPressed: ()=> Navigator.pop(context)
);
}
}
~~~
運行一下代碼,效果如下:
:-: 
圖 1 基本路由示例
可以看到,基本路由的使用還是比較簡單的。接下來,我們再看看命名路由的使用方法。
### 命名路由
基本路由使用方式相對簡單靈活,適用于應用中頁面不多的場景。而在應用中頁面比較多的情況下,再使用基本路由方式,那么每次跳轉到一個新的頁面,我們都要手動創建 MaterialPageRoute 實例,初始化頁面,然后調用 push 方法打開它,還是比較麻煩的。
所以,Flutter 提供了另外一種方式來簡化路由管理,即命名路由。我們給頁面起一個名字,然后就可以直接通過頁面名字打開它了。這種方式簡單直觀,**與 React 中的 navigation 使用方式類似**。
要想通過名字來指定頁面切換,我們必須先給應用程序 MaterialApp 提供一個頁面名稱映射規則,即路由表 routes,這樣 Flutter 才知道名字與頁面 Widget 的對應關系。
路由表實際上是一個 Map,其中 key 值對應頁面名字,而 value 值則是一個 WidgetBuilder 回調函數,我們需要在這個函數中創建對應的頁面。而一旦在路由表中定義好了頁面名字,我們就可以使用 Navigator.pushNamed 來打開頁面了。
下面的代碼演示了命名路由的使用方法:在 MaterialApp 完成了頁面的名字 second\_page 及頁面的初始化方法注冊綁定,后續我們就可以在代碼中以 second\_page 這個名字打開頁面了:
~~~
MaterialApp(
...
// 注冊路由
routes:{
"second_page":(context)=>SecondPage(),
},
);
// 使用名字打開頁面
Navigator.pushNamed(context,"second_page");
~~~
可以看到,命名路由的使用也很簡單。
不過**由于路由的注冊和使用都采用字符串來標識,這就會帶來一個隱患**:如果我們打開了一個不存在的路由會怎么辦?
也許你會想到,我們可以約定使用字符串常量去定義、使用路由,但我們無法避免通過接口數據下發的錯誤路由標識符場景。面對這種情況,無論是直接報錯或是不響應錯誤路由,都不是一個用戶體驗良好的解決辦法。
**更好的辦法是**,對用戶進行友好的錯誤提示,比如跳轉到一個統一的 NotFoundScreen 頁面,也方便我們對這類錯誤進行統一收集、上報。
在注冊路由表時,Flutter 提供了 UnknownRoute 屬性,我們可以對未知的路由標識符進行統一的頁面跳轉處理。
下面的代碼演示了如何注冊錯誤路由處理。和基本路由的使用方法類似,我們只需要返回一個固定的頁面即可。
~~~
MaterialApp(
...
// 注冊路由
routes:{
"second_page":(context)=>SecondPage(),
},
// 錯誤路由處理,統一返回 UnknownPage
onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);
// 使用錯誤名字打開頁面
Navigator.pushNamed(context,"unknown_page");
~~~
運行一下代碼,可以看到,我們的應用不僅可以處理正確的頁面路由標識,對錯誤的頁面路由標識符也可以統一跳轉到固定的錯誤處理頁面了。
:-: 
圖 2 命名路由示例
### 頁面參數
與基本路由能夠精確地控制目標頁面初始化方式不同,命名路由只能通過字符串名字來初始化固定目標頁面。為了解決不同場景下目標頁面的初始化需求,Flutter 提供了路由參數的機制,可以在打開路由時傳遞相關參數,在目標頁面通過 RouteSettings 來獲取頁面參數。
下面的代碼演示了如何傳遞并獲取參數:使用頁面名稱 second\_page 打開頁面時,傳遞了一個字符串參數,隨后在 SecondPage 中,我們取出了這個參數,并將它展示在了文本中。
~~~
// 打開頁面時傳遞字符串參數
Navigator.of(context).pushNamed("second_page", arguments: "Hey");
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 取出路由參數
String msg = ModalRoute.of(context).settings.arguments as String;
return Text(msg);
}
}
~~~
除了頁面打開時需要傳遞參數,對于特定的頁面,在其關閉時,也需要傳遞參數告知頁面處理結果。
比如在電商場景下,我們會在用戶把商品加入購物車時,打開登錄頁面讓用戶登錄,而在登錄操作完成之后,關閉登錄頁面返回到當前頁面時,登錄頁面會告訴當前頁面新的用戶身份,當前頁面則會用新的用戶身份刷新頁面。
與 Android 提供的 startActivityForResult 方法可以監聽目標頁面的處理結果類似,Flutter 也提供了**返回參數**的機制。在 push 目標頁面時,可以設置目標頁面關閉時監聽函數,以獲取返回參數;而目標頁面可以在關閉路由時傳遞相關參數。
下面的代碼演示了如何獲取參數:在 SecondPage 頁面關閉時,傳遞了一個字符串參數,隨后在上一頁監聽函數中,我們取出了這個參數,并將它展示了出來。
~~~
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Text('Message from first screen: $msg'),
RaisedButton(
child: Text('back'),
// 頁面關閉時傳遞參數
onPressed: ()=> Navigator.pop(context,"Hi")
)
]
));
}
}
class _FirstPageState extends State<FirstPage> {
String _msg='';
@override
Widget build(BuildContext context) {
return new Scaffold(
body: Column(children: <Widget>[
RaisedButton(
child: Text('命名路由(參數 & 回調)'),
// 打開頁面,并監聽頁面關閉時傳遞的參數
onPressed: ()=> Navigator.pushNamed(context, "third_page",arguments: "Hey").then((msg)=>setState(()=>_msg=msg)),
),
Text('Message from Second screen: $_msg'),
],),
);
}
}
~~~
運行一下,可以看到在關閉 SecondPage,重新回到 FirstPage 頁面時,FirstPage 把接收到的 msg 參數展示了出來:
:-: 
圖 3 頁面路由參數
## 總結
好了,今天的分享就到這里。我們簡單回顧一下今天的主要內容吧。
Flutter 提供了基本路由和命名路由兩種方式,來管理頁面間的跳轉。其中,基本路由需要自己手動創建頁面實例,通過 Navigator.push 完成頁面跳轉;而命名路由需要提前注冊頁面標識符和頁面創建方法,通過 Navigator.pushNamed 傳入標識符實現頁面跳轉。
對于命名路由,如果我們需要響應錯誤路由標識符,還需要一并注冊 UnknownRoute。為了精細化控制路由切換,Flutter 提供了頁面打開與頁面關閉的參數機制,我們可以在頁面創建和目標頁面關閉時,取出相應的參數。
可以看到,關于路由導航,Flutter 綜合了 Android、iOS 和 React 的特點,簡潔而不失強大。
而在中大型應用中,我們通常會使用命名路由來管理頁面間的切換。命名路由的最重要作用,就是建立了字符串標識符與各個頁面之間的映射關系,使得各個頁面之間完全解耦,應用內頁面的切換只需要通過一個字符串標識符就可以搞定,為后期模塊化打好基礎。
我把今天分享所涉及的的知識點打包到了[GitHub](https://github.com/cyndibaby905/21_router_demo)上,你可以下載工程到本地,多運行幾次,從而加深對基本路由、命名路由以及路由參數具體用法的印象。
## 思考題
最后,我給你留下兩個小作業吧。
1. 對于基本路由,如何傳遞頁面參數?
2. 請實現一個計算頁面,這個頁面可以對前一個頁面傳入的 2 個數值參數進行求和,并在該頁面關閉時告知上一頁面計算的結果。
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略