Flutter之Navigator原始碼解析

大逗大人發表於2019-12-26

Flutter中,頁面的跳轉是通過Navigator來實現。通過幾句簡單的程式碼就可以實現頁面的跳轉並傳遞對應的引數。那麼具體實現是怎樣的尼?下面就來一窺究竟。

1、根Navigator

Flutter中,一切皆WidgetNavigator也不能例外。但我們並沒有主動新增Navigator,但是又可以通過Navigator來進行頁面跳轉,這是怎麼回事尼?

我們在進行Flutter開發時,必須以MaterialAppWidgetsApp來作為第一個Widget,否則就會出錯。也就是在WidgetsApp中(MaterialApp是對WidgetsApp的包裝),Flutter預設給我們新增了第一個Navigator,一般跳轉都是根據這個Navigator來進行的。這也就是我們沒有主動新增Navigator,但卻能夠通過Navigator來進行頁面跳轉的原因。如下圖所示。

Flutter之Navigator原始碼解析
Flutter之Navigator原始碼解析
從圖中可以清晰的看到存在於Widget樹中的第一個Navigator,即根Navigator

對於Navigator,我們一般都是通過Navigator.of(context)來獲取NavigatorState物件,然後通過NavigatorState物件中的函式來實現頁面跳轉、關閉、替換等。下面來看Navigator.of(context)的實現。

  static NavigatorState of(
    BuildContext context, {
    bool rootNavigator = false,
    bool nullOk = false,
  }) {
    final NavigatorState navigator = rootNavigator
        //獲取根Navigator
        ? context.findRootAncestorStateOfType<NavigatorState>()
        //獲取距離當前Widget最近的Navigator
        : context.findAncestorStateOfType<NavigatorState>();
    return navigator;
  }
複製程式碼

rootNavigatortrue表示獲取根Navigator,否則獲取距離當前Widget最近的Navigator,預設為false

可以發現,這裡要想獲取根Navigator,就需要傳遞一個context物件,但在某些需求(如token失效跳轉登入頁)下,無法拿到context物件,但又需要跳轉,那這該怎麼辦尼?

這時候我們可以給根Navigator傳遞一個key,然後根據這個key拿到NavigatorState物件。而MaterialApp又給我們提供了向根Navigator傳遞key的機會。

class MaterialApp extends StatefulWidget {
  const MaterialApp({
    Key key,
    //自定義key
    this.navigatorKey,
    ...
  })
}
複製程式碼

通過給navigatorKey賦一個GlobalKey物件,然後通過這個物件就可以不需要context來獲得根Navigator,從而進行頁面的跳轉、返回、替換等。

2、push頁面原始碼解析

當我們正確的拿到NavigatorState物件後,就可以通過push函式來跳轉到到新的頁面。

  @optionalTypeArgs
  Future<T> push<T extends Object>(Route<T> route) {
    //獲取上一個route
    final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
    route._navigator = this;
    //獲取當前OverlayEntry物件
    route.install(_currentOverlayEntry);
    //將當前route新增到集合中
    _history.add(route);
    route.didPush();
    route.didChangeNext(null);
    if (oldRoute != null) {
      oldRoute.didChangeNext(route);
      route.didChangePrevious(oldRoute);
    }
    for (NavigatorObserver observer in widget.observers)
      observer.didPush(route, oldRoute);
    RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);
    _afterNavigation(route);
    return route.popped;
  }
複製程式碼

在這裡,route(路由)是一個非常重要的概念,它對頁面進行了抽象。在Flutter中,一個頁面對應一個route物件。_historyNavigatorState中的一個陣列,也是通常所說的路由棧。在跳轉一個新頁面時,會將route物件新增到_history陣列中,彈出頁面則從該陣列中移除route物件。

在上面程式碼中,route.install(_currentOverlayEntry)是頁面跳轉的核心實現。install函式在Route中是一個空實現,在其子類ModalRoute中進行了具體實現。

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {

  ......
  
  @override
  void install(OverlayEntry insertionPoint) {
    super.install(insertionPoint);
    _animationProxy = ProxyAnimation(super.animation);
    _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
  }
  ......
}
複製程式碼

ModalRoute裡的install函式中主要做了動畫相關的處理,然後交給父類執行。在其父類TransitionRoute主要是做動畫的建立及相關處理,並交給其父類處理。那麼再來看TransitionRoute的父類OverlayRoute

abstract class OverlayRoute<T> extends Route<T> {

  .....

  /// Subclasses should override this getter to return the builders for the overlay.
  Iterable<OverlayEntry> createOverlayEntries();

  /// The entries this route has placed in the overlay.
  @override
  List<OverlayEntry> get overlayEntries => _overlayEntries;
  final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];

  @override
  void install(OverlayEntry insertionPoint) {
    //建立頁面對應的兩個OverlayEntry物件並加入到_overlayEntries中,以待後續處理。
    _overlayEntries.addAll(createOverlayEntries());
    //進行UI的更新
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
    //空實現
    super.install(insertionPoint);
  }
  
  ......
  
}
複製程式碼

OverlayRoute中有一個抽象函式createOverlayEntries,該函式需要在子類實現。程式碼如下。

  @override
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }
複製程式碼

yield的含義先不管。該函式主要是建立兩個OverlayEntry物件,一個是我們需要展示頁面所對應的OverlayEntry物件,一個是Barrier所對應的OverlayEntry物件。它兩的關係如下。

Flutter之Navigator原始碼解析
route所對應的兩個OverlayEntry物件建立成功後,就加入到集合 _overlayEntries中,然後呼叫OverlayStateinsertAll函式。

class OverlayState extends State<Overlay> with TickerProviderStateMixin {
  final List<OverlayEntry> _entries = <OverlayEntry>[];

  ......

  void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above }) {
    if (entries.isEmpty)
      return;
    for (OverlayEntry entry in entries) {
      entry._overlay = this;
    }
    //更新UI
    setState(() {
      _entries.insertAll(_insertionIndex(below, above), entries);
    });
  }
  
  @override
  Widget build(BuildContext context) {
    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(_OverlayEntry(entry));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
      }
    }
    return _Theatre(
      onstage: Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      offstage: offstageChildren,
    );
  }

  ......
  
}
複製程式碼

insertAll裡,就可以發現頁面跳轉的本質了。就是通過setState來更新UI。

Flutter之Navigator原始碼解析

再來看OverlayStatebuild的函式,在該函式中會對需要儲存在記憶體中的頁面進行分類,主要有以下兩類。

  • onstageChildren:在該集合中的頁面會展示在螢幕中,也就是我們能夠看到的頁面。
  • offstageChildren:該集合中的頁面存在於記憶體中,不會銷燬,也不會展示。主要目的是為了下次載入時,方便重新載入,從而達到節省資源的目的。當maintainState為true時就會新增到該集合,目前面跳轉的maintainState引數預設為true。

來看一個示例。假設有三個頁面page1、page2及page3。我們先給page2頁面的maintainState屬性設定不同值,然後觀察page2跳轉到page3時page2頁面的變化情況。

Flutter之Navigator原始碼解析

Flutter之Navigator原始碼解析

可以發現,當maintainState為true時,會在記憶體中快取page2頁面的所有物件,以便下次操作。當然如果後面不需要再次展示page2,那麼就最好將maintainState設為false。

3、pop頁面原始碼解析

前面分析了在flutter中如何跳轉一個新的頁面,那麼來看一下如何關閉一個頁面。關閉頁面是呼叫的pop函式。

  bool pop<T extends Object>([ T result ]) {
    //從路由棧中獲取當前頁面對應的route物件
    final Route<dynamic> route = _history.last;
    bool debugPredictedWouldPop;
    if (route.didPop(result ?? route.currentResult)) {
      if (_history.length > 1) {
        //從集合中移除當前頁面所對應的route物件
        _history.removeLast();
        // If route._navigator is null, the route called finalizeRoute from
        // didPop, which means the route has already been disposed and doesn't
        // need to be added to _poppedRoutes for later disposal.
        if (route._navigator != null)
          _poppedRoutes.add(route);
        _history.last.didPopNext(route);
        for (NavigatorObserver observer in widget.observers)
          observer.didPop(route, _history.last);
        RouteNotificationMessages.maybeNotifyRouteChange(_routePoppedMethod, route, _history.last);
      } else {
        return false;
      }
    } else {}
    _afterNavigation<dynamic>(route);
    return true;
  }
複製程式碼

這裡重點是didPop這個函式。該函式中會呼叫NavigatorStatefinalizeRoute函式來釋放資源。而finalizeRoute又會呼叫routedispose函式。

abstract class OverlayRoute<T> extends Route<T> {

  @override
  bool didPop(T result) {
    final bool returnValue = super.didPop(result);
    assert(returnValue);
    //釋放資源操作
    if (finishedWhenPopped)
      navigator.finalizeRoute(this);
    return returnValue;
  }

  //釋放資源
  @override
  void dispose() {
    //從_overlayEntries集合中移除當前頁面及Barrier層
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
    super.dispose();
  }
}
複製程式碼

dispose函式中,會將_overlayEntries集合中清空,這裡之所以是集合並遍歷,是因為一個頁面對應2個OverlayEntry物件。

  void remove() {
    final OverlayState overlay = _overlay;
    //取消OverlayEntry對OverlayState物件的引用
    _overlay = null;
    if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      //如果當前正在渲染UI,則會等待渲染完畢後,才更新。
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        //更新UI
        overlay._remove(this);
      });
    } else {
      //更新UI
      overlay._remove(this);
    }
  }
複製程式碼

再來看OverlayState_remove函式,該函式也是呼叫了setState函式。然後等待UI的重新整理即可。

class OverlayState extends State<Overlay> with TickerProviderStateMixin {
  final List<OverlayEntry> _entries = <OverlayEntry>[];
  
  ......
  
  void _remove(OverlayEntry entry) {
    if (mounted) {
      setState(() {
        _entries.remove(entry);
      });
    }
  }
  
  ......
}
複製程式碼

再來看上一個示例,在page2頁面關閉時的變化情況。

Flutter之Navigator原始碼解析

Flutter之Navigator原始碼解析

可以發現,當maintainState為false時,從page3返回page2,會重新建立page2頁面的所有物件。如果page2頁面比較複雜,那麼此舉就比較耗資源,想必這也是maintainState預設為true的原因吧。

4、總結

當然,Navigator還有其他實現函式,如pushNamed(根據routeName跳轉)、pushAndRemoveUntil(關閉並跳轉到新頁面)、replace(替換當前頁面)等。但這些函式的具體實現跟pushpop的實現基本上一致,所以只要理解了flutter中如何進行頁面跳轉,那麼也就基本上了解了Navigator的的實現原理。也就知道了彈窗的開啟、關閉的實現原理,因為這些操作也是通過Navigator來實現的。

【參考資料】

Flutter進階:路由、路由棧詳解及案例分析

Flutter 路由原理解析

相關文章