庖丁解牛 · 如何理解 Flutter 路由源碼設計? | 創做者訓練營第二期

庖丁解牛·如何理解 Flutter 路由源碼設計?

學習最忌盲目,無計劃,零碎的知識點沒法串成系統。學到哪,忘到哪,面試想不起來。這裏我整理了Flutter面試中最常問以及Flutter framework中最核心的幾塊知識,歡迎關注,共同進步。image.png 歡迎搜索公衆號:進擊的Flutter或者runflutter 裏面整理收集了最詳細的Flutter進階與優化指南。關注我,探討你的問題,獲取個人最新文章~git

本期看點: 一、70行代碼實現一個丐版路由 二、路由源碼細節解析github

導語:

某天在公衆號看到這樣一個問題面試

image.png

這問題我熟啊,恰好翻譯 OverlayEntries 和 Routes 進行了重建優化 裏面提到,在 1.17 版本以後,當咱們打開一個新頁面(Route),前一個頁面將再也不從新構建markdown

爲了搞清楚前一個頁面爲何build,我基於 1.12.13 版本寫了個 demo 測試,結果發現不止是前一個頁面會再次 build,前面全部的頁面都會 build框架

咱們第一印象可能會以爲上一頁面被覆蓋了就不該該再次構建了!這是發生了什麼?要搞清這個問題可還真不那麼容易,劃分兩期來分析原理。本篇會和你們從源碼分析一個最熟悉的陌生人路由async


1、初識路由:一種頁面切換的能力

爲何先分析路由,由於問題發生在頁面切換的場景下。ide

Flutter 中咱們每每經過這樣一行代碼svn

image.png

打開到一個新的頁面 PageE,調用 Navigator.of(context).pop() 退出一個頁面。 因此路由簡單來講,就是一種頁面切換的能力oop

Flutter 如何實現這一能力?爲了更深入理解源碼設計,本期咱們換個思路,讓咱們拋開如今的路由機制思考:假如 framework 移除了路由機制,你會如何實現頁面切換?源碼分析


2、如何實現一個丐版路由

一、設計路由容器

爲了管理每一個頁面的退出和進入,咱們能夠設計一個路由容器進行管理,那這個容器該如何設計? 觀察頁面打開和關閉這兩個過程,其實很是簡單。打開就是目標頁面覆蓋了上一個頁面,而退出過程則恰好相反。

untitled.gif

根據系統現有的 Widget 咱們很天然想到了 Stack,Stack 相似原生的相對佈局,每一個 Widget 能夠根據本身的位置疊加顯示在屏幕上。只要咱們把它的每一個子 widget 都撐滿,那麼 Stack 每次只會顯示最後一個 widget,這不就相似每次打開一個頁面麼。

class RouteHostState extends State<RouteHost> with TickerProviderStateMixin {
  List<Widget> pages = []; //路由中的多個頁面
  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand, //每一個頁面撐滿屏幕
      children: pages,
    );
  }
}
複製代碼

二、提供頁面切換方法

由於容器基於 Stack 因此打開和關閉頁面也很是簡單。對於打開一個頁面咱們只須要將新的頁面添加到 pages 中;關閉頁面,咱們只要移除最後一個便可。爲了讓切換過程更加流暢,能夠添加一些動畫轉場效果。

以打開頁面爲例其實只需三步

Step 一、建立一個轉場動畫

//一、建立一個位移動畫
    AnimationController animationController;
    Animation<Offset> animation;
    animationController = AnimationController(
          vsync: this, duration: Duration(milliseconds: 500));
    animation = Tween(begin: Offset(1, 0), end: Offset.zero)
          .animate(animationController);
複製代碼

Step 二、將目標頁面添加到 stack 中顯示

//二、添加到 stack 中並顯示
    pages.add(SlideTransition(
      position: animation,
      child: page,
    ));
複製代碼

Step 三、開啓轉場動畫

//三、調用 setState 並開啓轉場動畫
    setState(() {
        animationController.forward();
    }
複製代碼

是的,簡單來講只須要這三步便可完成,咱們能夠看看效果 打開路由.gif

關閉頁面則反過來便可。

//關閉最後一個頁面
  void close() async {
      //出場動畫
      await controllers.last.reverse();
      //移除最後一個頁面
      pages.removeLast();
      controllers.removeLast().dispose();
  }
}
複製代碼

三、讓子頁面使用路由能力

上面咱們提到打開關和閉頁面方法都在路由容器中,那子頁面如何能使用這個能力?這個問題背後實際上是 Flutter 中一個頗有意思的話題,父子節點如何數據傳遞?

咱們知道 Flutter 框架體系中有三棵樹,在Widget、Element、Render是如何造成樹結構?中熟悉了它們的構建過程。 Flutter 提供了多個方法讓咱們能夠訪問父子節點:

abstract class BuildContext {
  ///查找父節點中的T類型的State
  T findAncestorStateOfType<T extends State>();
  ///查找父節點中的T類型的 InheritedWidget 例如 MediaQuery 等
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect })
  ///遍歷子元素的element對象
  void visitChildElements(ElementVisitor visitor);
  ......
}
複製代碼

源碼中例如咱們常使用的NavigatorMediaQueryInheritedTheme,以及不少狀態管理框架也是基於這個原理實現。一樣的,能夠經過這樣的方法將路由能力提供給子頁面。

///RouteHost提供給子節點訪問本身 State 的能力
  static RouteHostState of(BuildContext context) {
    return context.findAncestorStateOfType<RouteHostState>();
  }
  ///子節點藉助上面的方法使用路由
  void openPage() {
    RouteHost.of(context).open(RoutePage());
  }  
複製代碼

最後咱們看看實際打開和關閉的效果:

完整案例.gif

完整案例在 github.com/Nayuta403/f…;


3、理解路由源碼設計

有了上面的思考,那麼對於源碼的設計咱們就很清晰了。 如今咱們回過頭來看看路由的使用

Navigator.of(context).push(MaterialPageRoute(builder: (c) {
      return PageB();
    }));
複製代碼

對比咱們設計的路由,來拆解原理。

RouteHost.of(context).open(RoutePage());
複製代碼

路由容器:Navigator

對比兩個方法, 其實咱們就明白了Navigator就是起到路由容器的做用。查看源碼你會發現,他被嵌套在 MaterialApp 中,而且 Nagivator 內部也是經過 Stack 實現。

image.png

咱們的每個頁面都是 Navigator 的子節點,天然能夠經過 context 去獲取它。

static NavigatorState of(BuildContext context) {
    ///獲取位於根部的 Navigator
    final NavigatorState navigator = rootNavigator
        ? context.findRootAncestorStateOfType<NavigatorState>()
        : context.findAncestorStateOfType<NavigatorState>();
    return navigator;
  }
複製代碼

Route:處理頁面轉場等設計

明白了 Navigator 以後,咱們發現每次打開頁面的時候每每須要傳入 PageRoute 對象,這又起到什麼做用呢?

在咱們上面的設計中,爲了讓過渡天然,咱們在 open 方法中,手動的爲每個頁面添加了轉場動畫。 而 Flutter 中將路由切換所需的動畫,交互阻斷等封裝成了 Route 對象。經過層次封裝的形式,逐層實現了這些能力:

image.png

有了前面的思考以後,再看路由源碼的設計,思路其實變得很是清晰。對於源碼的學習,千萬不要一開始深陷在細節中,從總體思考再拆解流程,這樣方可深刻淺出。


4、源碼中的細節

有了總體大框架以後,咱們能夠具體梳理 Navigator.of(context).push 過程。

Future<T> push<T extends Object>(Route<T> route) {
    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);
      route.didChangePrevious(oldRoute);
    }
    /// 三、回調 Navigator 的觀察者
    for (NavigatorObserver observer in widget.observers)
      observer.didPush(route, oldRoute);
    RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);
    _afterNavigation(route);
    return route.popped;
  }
複製代碼

這裏咱們只需關注核心的第一個過程,關鍵方法在:

route.install(_currentOverlayEntry); 
複製代碼

這個方法被 Route 的子類重寫,而且分層完成了不一樣邏輯: image.png

在OverlayRoute中以下:

void install(OverlayEntry insertionPoint) {
    /// 經過 createOverlayEntries() 建立新頁面的 _overlayEntries 集合
    /// 這個 _overlayEntries 集合就是咱們打開的新頁面
    _overlayEntries.addAll(createOverlayEntries());
    /// 將新頁面的 _overlayEntries 集合插入到 overlay 中顯示
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
    super.install(insertionPoint);
  }
  
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    /// 建立一個遮罩
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    /// 建立頁面實際內容,最終調用到 Route 的 builder 方法
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }
複製代碼

第一行代碼中的 createOverlayEntries() 方法會先建立一個zhe調用到 Route 的 builder 方法,建立咱們須要打開的頁面與遮罩,以後將整個集合添加到 Overlay 中(若是不太熟悉 Overlay 將它當作一個 Stack 就行)。

/// overlay.dart
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above }) {
    setState(() {
      _entries.insertAll(_insertionIndex(below, above), entries);
    });
  }
複製代碼

overlay 的方法也很簡單,添加頁面到 _entries 調用 setState() 更新。 這個 _entries 簡單來看,這個 _entries 就和咱們前面設計的 pages 相似,不過裏面多了 選擇渲染 的能力,咱們下一期再詳細分析。


5、總結

看到這,相信你對於 Flutter 中的路由不再會感到陌生,總結下來關鍵有三點:

  • 一、Navigator 做爲路由容器內部嵌套了 Stack 提供了頁面切換的能力。
  • 二、經過context.findRootAncestorStateOfType()能夠訪問父節點
  • 三、Route 爲咱們封裝了切換時須要的其餘能力

固然其中還有一些細節,例如 Overlay 是什麼,頁面的生命週期是如何切換,留着下期和你們在分析了。下期內容比較深,先熟悉一下 Flutter 的渲染機制收穫會更多哦~

推薦閱讀:


6、最後 感謝各位吳彥祖和彭于晏的點贊和關注

當咱們切換頁面的時候,上一個頁面默認會走如下幾個生命週期:

image.png

這又是爲何?必定是這樣的順序麼?Flutter 生命週期到底該怎麼回答? 咱們留着下一期再分析拉~

若是你以爲文章寫得還不錯~ 點個關注、點個贊啦,彥祖~

歡迎搜索公衆號:進擊的Flutter或者 runflutter 裏面整理收集了最詳細的Flutter進階與優化指南。關注我,獲取個人最新文章~

週末參加了掘金的創做者活動技術創做者們,快來這裏交做業啦 | 創做者訓練營第二期 ,多位行業大牛介紹了技術寫做、思惟提高、職場晉升等等心得,受益不淺。連接中有錄播,一塊兒提筆開啓你的寫做之旅吧。

相關文章
相關標籤/搜索