Flutter中的導航和路由簡析

開始

不管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

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的繼承關係:

clipboard.png

能夠看到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跳轉就有點出入了,值得注意。

相關文章
相關標籤/搜索