不管app仍是webapp,路由都是必不可少的,相對於webapp,app的路由通常都更增強大和可控,這方面web實在太欠缺,而Flutter很明顯徹底克服了web的缺點,擁有一個更爲完善的路由模塊,這也是Flutter整個框架的特色,吸取web開發優勢,但也克服web那些顯而易見的缺點,提供一個更爲輕鬆高效的開發環境,好吧,接下來一塊兒深刻了解這個模塊吧。前端
能夠理解Flutter僅僅提供一個View層,其實至關多的功能都要依賴原生,例如電池信息,位置信息,網絡信息等等,更爲簡單的說Flutter就是Super WebView。
一樣當咱們按下返回鍵時,就須要原生層告訴Flutter,彈出一個路由,讓它返回上一級頁面。
來到WidgetsBinding.initInstances方法:web
void initInstances() { ... SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation); ... }
接着_handleNavigationInvocation方法:網絡
Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) { switch (methodCall.method) { case 'popRoute': return handlePopRoute(); case 'pushRoute': return handlePushRoute(methodCall.arguments); } return new Future<Null>.value(); }
根據原生層的調用選擇彈出一個路由或者壓入一個路由,如今主要追蹤彈出路由的處理。
接着handlePopRoute方法:app
Future<Null> handlePopRoute() async { for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) { if (await observer.didPopRoute()) return; } SystemNavigator.pop(); }
這裏也是一個觀察者模式的實現了,通知全部監聽者彈出路由,這裏主要一個處理就是若是didPopRoute返回false(也就是沒有路由願意處理)就交給系統默認處理,通常要麼退出這個Activity,要麼退出應用。
那麼在哪裏註冊這個監聽器並與Navigator組件關聯起來的尼?
答案就在_WidgetsAppState這類裏面:框架
void initState() { ... WidgetsBinding.instance.addObserver(this); }
再看一下這個類的didPopRoute和didPushRoute方法:webapp
// On Android: the user has pressed the back button. @override Future<bool> didPopRoute() async { final NavigatorState navigator = _navigator.currentState; return await navigator.maybePop(); } @override Future<bool> didPushRoute(String route) async { final NavigatorState navigator = _navigator.currentState; navigator.pushNamed(route); return true; }
在這裏就把Navigator關聯起來了。async
Navigator的職責是負責管理Route的,管理方式就是利用一個棧不停壓入彈出,固然也能夠直接替換其中某一個Route。而Route做爲一個管理單元,主要負責建立對應的界面,響應Navigator壓入路由和彈出路由。
Flutter定義路由的方式跟前端MVC框架是很類似的,你會看到有這種相似的:/home,/posts,/posts/:id等等,搞前端的同窗應該想到熟悉。ide
接着繼續追蹤路由彈出處理流程,看一下NavigatorState.maybePop方法:佈局
Future<bool> maybePop([dynamic result]) async { final Route<dynamic> route = _history.last; assert(route._navigator == this); final RoutePopDisposition disposition = await route.willPop(); if (disposition != RoutePopDisposition.bubble && mounted) { if (disposition == RoutePopDisposition.pop) pop(result); return true; } return false; }
這個時候,Navigator會詢問Route是否要本身處理仍是交給系統處理,當Route.willPop返回值爲RoutePopDisposition.bubble時即交給系統處理,這裏也簡單介紹RoutePopDisposition三個枚舉值:post
pop 彈出路由,正常狀況返回上一級
doNotPop 不彈出,沉默處理,不少時候出如今一些表單填寫的狀況,必須完成頁面內容,或者提示用戶點擊第二次才能退出
bubble 交給系統處理,通常直接退出應用
再看一下Route.willPop默認實現:
Future<RoutePopDisposition> willPop() async { return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop; }
先會判斷自身是不是最後一個路由,若是是交給系統處理退出應用,若是不是彈出一個路由,很正常的行爲實現。
因此當返回的是pop時,調用Navigator.pop方法:
bool pop([dynamic result]) { final Route<dynamic> route = _history.last; bool debugPredictedWouldPop; if (route.didPop(result ?? route.currentResult)) { if (_history.length > 1) { setState(() { _history.removeLast(); if (route._navigator != null) _poppedRoutes.add(route); _history.last.didPopNext(route); for (NavigatorObserver observer in widget.observers) observer.didPop(route, _history.last); }); } else { return false; } } else { assert(!debugPredictedWouldPop); } _cancelActivePointers(); return true; }
在彈出路由前,會調用Route.didPop方法,也能夠看到就算以前Route.willPop返回值爲pop,仍然能夠在Route.didPop返回false改變這個行爲,從而不彈出路由。
可是若是Route.didPop方法返回的是true,就會把當前路由彈出,並調起如今當前的路由didPopNext方法通知它已經回到前臺,作好一些狀態恢復工做,例如:拉取最新列表信息。
而最後的_cancelActivePointers方法,馬上分發一個PointerCancel事件,這個時候手勢識別器的狀態會被重置,例如:雙擊手勢,剛點了第一下就按了返回鍵,就會重置狀態。
接着Route.didPop方法,當咱們退出一個頁面的時候通常都會執行一個過渡動畫,可是過渡動畫的持續時間多少,Navigator沒法知道,因此Route要本身負責調起NavigatorState.finalizeRoute方法,通知Navigator釋放路由,而後Navigator會回調Route.dispose方法釋放Route自身資源,路由的生命週期結束。
直接來到NavigatorState.pushNamed方法:
Future<dynamic> pushNamed(String name) { return push(_routeNamed(name)); }
再跳到_routeNamed方法:
Route<dynamic> _routeNamed(String name, { bool allowNull: false }) { final RouteSettings settings = new RouteSettings( name: name, isInitialRoute: _history.isEmpty, ); Route<dynamic> route = widget.onGenerateRoute(settings); if (route == null && !allowNull) { route = widget.onUnknownRoute(settings); } return route; }
也很簡單根據名稱查找路由,能夠看看MaterialApp的實現:
Route<dynamic> _onGenerateRoute(RouteSettings settings) { final String name = settings.name; WidgetBuilder builder; if (name == Navigator.defaultRouteName && widget.home != null) builder = (BuildContext context) => widget.home; else builder = widget.routes[name]; if (builder != null) { return new MaterialPageRoute<dynamic>( builder: builder, settings: settings, ); } if (widget.onGenerateRoute != null) return widget.onGenerateRoute(settings); return null; }
咱們建立MaterialApp時都會傳入一個route的map,而後就是根據map來建立route,就這麼簡單。
若是找到路由,就像咱們訪問網頁404,應該給一個友好的頁面告訴用戶不存在,就能夠在onUnknownRoute回調中返回一個頁面。
接着NavigatorState.push方法:
Future<dynamic> push(Route<dynamic> route) { setState(() { final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null; route._navigator = this; route.install(_currentOverlayEntry); _history.add(route); route.didPush(); route.didChangeNext(null); if (oldRoute != null) oldRoute.didChangeNext(route); for (NavigatorObserver observer in widget.observers) observer.didPush(route, oldRoute); }); _cancelActivePointers(); return route.popped; }
重點看Route.install方法,首先了解一下Route的繼承關係:
能夠看到Route的實現都繼承自OverlayRoute,而OverlayRoute.install的實現:
void install(OverlayEntry insertionPoint) { assert(_overlayEntries.isEmpty); _overlayEntries.addAll(createOverlayEntries()); navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint); super.install(insertionPoint); }
navigator會把Route.createOverlayEntries建立的OverlayEntries添加到本身的Overlay組件上;而createOverlayEntries方法幹了啥尼,再來到ModalRoute.createOverlayEntries:
Iterable<OverlayEntry> createOverlayEntries() sync* { yield new OverlayEntry(builder: _buildModalBarrier); yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState); }
能夠看到構建兩個OverlayEntry,咱們常常看到對話框後面還有一層遮罩,就是由這裏產生的。
再看一下OverlayEntry有兩個重要的屬性opaque和maintainState,當咱們把OverlayEntry添加到Navigator的Overlay組件時,Overlay組件構建過程處理是這樣的:
Widget build(BuildContext context) { // These lists are filled backwards. For the offstage children that // does not matter since they aren't rendered, but for the onstage // children we reverse the list below before adding it to the tree. final List<Widget> onstageChildren = <Widget>[]; final List<Widget> offstageChildren = <Widget>[]; bool onstage = true; for (int i = _entries.length - 1; i >= 0; i -= 1) { final OverlayEntry entry = _entries[i]; if (onstage) { onstageChildren.add(new _OverlayEntry(entry)); if (entry.opaque) onstage = false; } else if (entry.maintainState) { offstageChildren.add(new TickerMode(enabled: false, child: new _OverlayEntry(entry))); } } return new _Theatre( onstage: new Stack( fit: StackFit.expand, children: onstageChildren.reversed.toList(growable: false), ), offstage: offstageChildren, ); }
當有一個OverlayEntry的opaque爲true時(就是不透明看不到下面的頁面),默認狀況下在它之下OverlayEntry不會實例化(也不必),可是若是設置maintainState爲true時,OverlayEntry會build出組件樹,可是這些組件不會被佈局和繪製,主要用於維持組件狀態。
當Route.dipose方法調起後,Route的OverlayEntry纔會從Navigator的Overlay組件移除。
再回頭當Route.install方法調起後,通常過渡動畫會在這裏構建,接會調用Route.didPush方法,過渡動畫會在這裏播放,最後再調用前一個路由的didChangeNext方法通知它被退到後臺,能夠在這個方法裏保存本身狀態信息,等下次回到前臺時恢復。
在咱們初始化的時候,咱們默認初始化路由是:‘/’,可是咱們若是初始化路由是這樣的:‘/posts/123’,框架是怎樣處理的尼?
咱們來到NavigatorState.initState方法
void initState() { super.initState(); for (NavigatorObserver observer in widget.observers) { assert(observer.navigator == null); observer._navigator = this; } String initialRouteName = widget.initialRoute ?? Navigator.defaultRouteName; if (initialRouteName.startsWith('/') && initialRouteName.length > 1) { initialRouteName = initialRouteName.substring(1); // strip leading '/' assert(Navigator.defaultRouteName == '/'); final List<String> plannedInitialRouteNames = <String>[ Navigator.defaultRouteName, ]; final List<Route<dynamic>> plannedInitialRoutes = <Route<dynamic>>[ _routeNamed(Navigator.defaultRouteName, allowNull: true), ]; final List<String> routeParts = initialRouteName.split('/'); if (initialRouteName.isNotEmpty) { String routeName = ''; for (String part in routeParts) { routeName += '/$part'; plannedInitialRouteNames.add(routeName); plannedInitialRoutes.add(_routeNamed(routeName, allowNull: true)); } } if (plannedInitialRoutes.contains(null)) { push(_routeNamed(Navigator.defaultRouteName)); } else { for (Route<dynamic> route in plannedInitialRoutes) push(route); //連續壓入 } } else { Route<dynamic> route; if (initialRouteName != Navigator.defaultRouteName) route = _routeNamed(initialRouteName, allowNull: true); if (route == null) route = _routeNamed(Navigator.defaultRouteName); push(route); } for (Route<dynamic> route in _history) _initialOverlayEntries.addAll(route.overlayEntries); }
整個處理也很簡單,直接把/posts和/posts/123壓入路由棧中,而不是僅僅只是把/posts/123壓進去,這跟web的url跳轉就有點出入了,值得注意。