在Flutter
中,頁面的跳轉是通過Navigator
來實現。通過幾句簡單的程式碼就可以實現頁面的跳轉並傳遞對應的引數。那麼具體實現是怎樣的尼?下面就來一窺究竟。
1、根Navigator
在Flutter
中,一切皆Widget
,Navigator
也不能例外。但我們並沒有主動新增Navigator
,但是又可以通過Navigator
來進行頁面跳轉,這是怎麼回事尼?
我們在進行Flutter
開發時,必須以MaterialApp
或WidgetsApp
來作為第一個Widget
,否則就會出錯。也就是在WidgetsApp
中(MaterialApp
是對WidgetsApp
的包裝),Flutter
預設給我們新增了第一個Navigator
,一般跳轉都是根據這個Navigator
來進行的。這也就是我們沒有主動新增Navigator
,但卻能夠通過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;
}
複製程式碼
rootNavigator
為true
表示獲取根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
物件。_history
是NavigatorState
中的一個陣列,也是通常所說的路由棧。在跳轉一個新頁面時,會將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
物件。它兩的關係如下。
route
所對應的兩個OverlayEntry
物件建立成功後,就加入到集合
_overlayEntries
中,然後呼叫OverlayState
的insertAll
函式。
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。
再來看OverlayState
的build
的函式,在該函式中會對需要儲存在記憶體中的頁面進行分類,主要有以下兩類。
- onstageChildren:在該集合中的頁面會展示在螢幕中,也就是我們能夠看到的頁面。
- offstageChildren:該集合中的頁面存在於記憶體中,不會銷燬,也不會展示。主要目的是為了下次載入時,方便重新載入,從而達到節省資源的目的。當
maintainState
為true時就會新增到該集合,目前面跳轉的maintainState
引數預設為true。
來看一個示例。假設有三個頁面page1、page2及page3。我們先給page2頁面的maintainState
屬性設定不同值,然後觀察page2跳轉到page3時page2頁面的變化情況。
可以發現,當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
這個函式。該函式中會呼叫NavigatorState
的finalizeRoute
函式來釋放資源。而finalizeRoute
又會呼叫route
的dispose
函式。
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頁面關閉時的變化情況。
可以發現,當maintainState
為false時,從page3返回page2,會重新建立page2頁面的所有物件。如果page2頁面比較複雜,那麼此舉就比較耗資源,想必這也是maintainState
預設為true的原因吧。
4、總結
當然,Navigator
還有其他實現函式,如pushNamed
(根據routeName跳轉)、pushAndRemoveUntil
(關閉並跳轉到新頁面)、replace
(替換當前頁面)等。但這些函式的具體實現跟push
及pop
的實現基本上一致,所以只要理解了flutter
中如何進行頁面跳轉,那麼也就基本上了解了Navigator
的的實現原理。也就知道了彈窗的開啟、關閉的實現原理,因為這些操作也是通過Navigator
來實現的。
【參考資料】