學習最忌盲目,無計劃,零碎的知識點沒法串成系統。學到哪,忘到哪,面試想不起來。這裏我整理了Flutter面試中最常問以及Flutter framework中最核心的幾塊知識,歡迎關注,共同進步。
歡迎搜索公衆號:進擊的Flutter或者runflutter 裏面整理收集了最詳細的Flutter進階與優化指南。關注我,探討你的問題,獲取個人最新文章~git
本期看點: 一、70行代碼實現一個丐版路由 二、路由源碼細節解析github
某天在公衆號看到這樣一個問題面試
這問題我熟啊,恰好翻譯 OverlayEntries 和 Routes 進行了重建優化 裏面提到,在 1.17 版本以後,當咱們打開一個新頁面(Route),前一個頁面將再也不從新構建。markdown
爲了搞清楚前一個頁面爲何build,我基於 1.12.13 版本寫了個 demo 測試,結果發現不止是前一個頁面會再次 build,前面全部的頁面都會 build。框架
咱們第一印象可能會以爲上一頁面被覆蓋了就不該該再次構建了!這是發生了什麼?要搞清這個問題可還真不那麼容易,劃分兩期來分析原理。本篇會和你們從源碼分析一個最熟悉的陌生人路由。async
爲何先分析路由,由於問題發生在頁面切換的場景下。ide
Flutter 中咱們每每經過這樣一行代碼svn
打開到一個新的頁面 PageE,調用 Navigator.of(context).pop()
退出一個頁面。 因此路由簡單來講,就是一種頁面切換的能力。oop
Flutter 如何實現這一能力?爲了更深入理解源碼設計,本期咱們換個思路,讓咱們拋開如今的路由機制思考:假如 framework 移除了路由機制,你會如何實現頁面切換?源碼分析
爲了管理每一個頁面的退出和進入,咱們能夠設計一個路由容器進行管理,那這個容器該如何設計? 觀察頁面打開和關閉這兩個過程,其實很是簡單。打開就是目標頁面覆蓋了上一個頁面,而退出過程則恰好相反。
根據系統現有的 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 中;關閉頁面,咱們只要移除最後一個便可。爲了讓切換過程更加流暢,能夠添加一些動畫轉場效果。
以打開頁面爲例其實只需三步
//一、建立一個位移動畫
AnimationController animationController;
Animation<Offset> animation;
animationController = AnimationController(
vsync: this, duration: Duration(milliseconds: 500));
animation = Tween(begin: Offset(1, 0), end: Offset.zero)
.animate(animationController);
複製代碼
//二、添加到 stack 中並顯示
pages.add(SlideTransition(
position: animation,
child: page,
));
複製代碼
//三、調用 setState 並開啓轉場動畫
setState(() {
animationController.forward();
}
複製代碼
是的,簡單來講只須要這三步便可完成,咱們能夠看看效果
關閉頁面則反過來便可。
//關閉最後一個頁面
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);
......
}
複製代碼
源碼中例如咱們常使用的Navigator
、MediaQuery
、InheritedTheme
,以及不少狀態管理框架也是基於這個原理實現。一樣的,能夠經過這樣的方法將路由能力提供給子頁面。
///RouteHost提供給子節點訪問本身 State 的能力
static RouteHostState of(BuildContext context) {
return context.findAncestorStateOfType<RouteHostState>();
}
///子節點藉助上面的方法使用路由
void openPage() {
RouteHost.of(context).open(RoutePage());
}
複製代碼
最後咱們看看實際打開和關閉的效果:
完整案例在 github.com/Nayuta403/f…;
有了上面的思考,那麼對於源碼的設計咱們就很清晰了。 如今咱們回過頭來看看路由的使用
Navigator.of(context).push(MaterialPageRoute(builder: (c) {
return PageB();
}));
複製代碼
對比咱們設計的路由,來拆解原理。
RouteHost.of(context).open(RoutePage());
複製代碼
對比兩個方法, 其實咱們就明白了Navigator就是起到路由容器的做用。查看源碼你會發現,他被嵌套在 MaterialApp 中,而且 Nagivator 內部也是經過 Stack 實現。
咱們的每個頁面都是 Navigator 的子節點,天然能夠經過 context 去獲取它。
static NavigatorState of(BuildContext context) {
///獲取位於根部的 Navigator
final NavigatorState navigator = rootNavigator
? context.findRootAncestorStateOfType<NavigatorState>()
: context.findAncestorStateOfType<NavigatorState>();
return navigator;
}
複製代碼
明白了 Navigator 以後,咱們發現每次打開頁面的時候每每須要傳入 PageRoute
對象,這又起到什麼做用呢?
在咱們上面的設計中,爲了讓過渡天然,咱們在 open 方法中,手動的爲每個頁面添加了轉場動畫。 而 Flutter 中將路由切換所需的動畫,交互阻斷等封裝成了 Route 對象。經過層次封裝的形式,逐層實現了這些能力:
有了前面的思考以後,再看路由源碼的設計,思路其實變得很是清晰。對於源碼的學習,千萬不要一開始深陷在細節中,從總體思考再拆解流程,這樣方可深刻淺出。
有了總體大框架以後,咱們能夠具體梳理 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 的子類重寫,而且分層完成了不一樣邏輯:
在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
相似,不過裏面多了 選擇渲染 的能力,咱們下一期再詳細分析。
看到這,相信你對於 Flutter 中的路由不再會感到陌生,總結下來關鍵有三點:
固然其中還有一些細節,例如 Overlay 是什麼,頁面的生命週期是如何切換,留着下期和你們在分析了。下期內容比較深,先熟悉一下 Flutter 的渲染機制收穫會更多哦~
推薦閱讀:
當咱們切換頁面的時候,上一個頁面默認會走如下幾個生命週期:
這又是爲何?必定是這樣的順序麼?Flutter 生命週期到底該怎麼回答? 咱們留着下一期再分析拉~
若是你以爲文章寫得還不錯~ 點個關注、點個贊啦,彥祖~
歡迎搜索公衆號:進擊的Flutter或者 runflutter 裏面整理收集了最詳細的Flutter進階與優化指南。關注我,獲取個人最新文章~
週末參加了掘金的創做者活動技術創做者們,快來這裏交做業啦 | 創做者訓練營第二期 ,多位行業大牛介紹了技術寫做、思惟提高、職場晉升等等心得,受益不淺。連接中有錄播,一塊兒提筆開啓你的寫做之旅吧。