Flutter中的Navigator和Route

zcchongpangzi發表於2020-04-14

導航和路由,APP開發都會遇到,承載了介面跳轉的邏輯,最近看了下Flutter中的Navigator,總結了一些簡單知識和大家分享,幫助大家快速瞭解和上手Navigator。首先看兩個問題:

home 和 initialRoute 都有,初始化頁面會是哪一個?

Navigator.pushNamed不傳context可以嗎?為什麼要傳context?

如果說不能很清楚的回答這兩個問題,這篇文章會給你幫助

Navigator和Route簡介

先看下定義

Navigator

A widget that manages a set of child widgets with a stack discipline.

Many apps have a navigator near the top of their widget hierarchy in order to display their logical history using an Overlay with the most recently visited pages visually on top of the older pages. Using this pattern lets the navigator visually transition from one page to another by moving the widgets around in the overlay. Similarly, the navigator can be used to show a dialog by positioning the dialog widget above the current page.

Route

An abstraction for an entry managed by a Navigator. This class defines an abstract interface between the navigator and the "routes" that are pushed on and popped off the navigator. Most routes have visual affordances, which they place in the navigators Overlay using one or more OverlayEntry objects.

這是flutter官方文件給出的定義,介紹了這兩個類的用途,具體什麼意思自己翻譯吧。。官方文件還給出了基本的使用例子,如果說只是簡單的進行頁面的跳轉,看官方例子就夠了

void main() {
  runApp(MaterialApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}
複製程式碼

使用name跳轉

Navigator.pushNamed(context, '/b');
複製程式碼

關於MaterialApp

MaterialApp中有一本分是關於navigator的引數(關於路由和頁面跳轉的,不探討頁面具體建立部分),MaterialApp是繼承自StatefulWidget的,去看_MaterialAppState的邏輯

 const MaterialApp({
    this.navigatorKey,// GlobalKey 用來引用Navigator
    this.home, //初始頁面
    this.routes = const <String, WidgetBuilder>{},// 路由
    this.initialRoute,//初始路由名稱
    this.onGenerateRoute,//路由生成
    this.onUnknownRoute,//未知路由顯示
    this.navigatorObservers = const <NavigatorObserver>[],//監聽
  })
複製程式碼

下面看_MaterialAppState,buid方法中有WidgetsApp類(是StatefulWidget),只保留navigator相關的引數如下,這裡需要注意pageRouteBuilder後面會說

 Widget build(BuildContext context) {
    Widget result = WidgetsApp(
      key: GlobalObjectKey(this),
      navigatorKey: widget.navigatorKey,
      navigatorObservers: _navigatorObservers,
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
        return MaterialPageRoute<T>(settings: settings, builder: builder);
      },
      home: widget.home,
      routes: widget.routes,
      initialRoute: widget.initialRoute,
      onGenerateRoute: widget.onGenerateRoute,
      onUnknownRoute: widget.onUnknownRoute,
    );
}
複製程式碼

之後再點進去,對應的_WidgetsAppState中,buid方法建立了navigator,下面是部分程式碼,這就是為什麼不需要自己去例項化navigator就可以跳轉的原因

  Widget build(BuildContext context) {
    Widget navigator;
    if (_navigator != null) {
      navigator = Navigator(
        key: _navigator,
        initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
            ? WidgetsBinding.instance.window.defaultRouteName
            : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
        onGenerateRoute: _onGenerateRoute,
        onUnknownRoute: _onUnknownRoute,
        observers: widget.navigatorObservers,
      );
    }
}
複製程式碼

初始化

初始化的過程

我們來看下路初始化的過程:在_WidgetsAppState的build方法中,例項化了Navigator,其中的key就是MaterialApp中傳入的navigatorKey,用來獲取navigator;來看下Navigator

const Navigator({
    Key key,
    this.initialRoute,
    @required this.onGenerateRoute,
    this.onUnknownRoute,
    this.observers = const <NavigatorObserver>[],
  }) : assert(onGenerateRoute != null),
       super(key: key);
}
複製程式碼

是StatefulWidget的子類,往下翻去看NavigatorState的方法,在initState中有第一個路由初始化的邏輯;首先取initialRoute,即MaterialApp中的initialRoute,沒有的話使用Navigator.defaultRouteName即'/'

String initialRouteName = widget.initialRoute ?? Navigator.defaultRouteName;
複製程式碼

之後又對initialRouteName分了三種

1、 '/' : 呼叫_routeNamed(Navigator.defaultRouteName, arguments: null)生成Route

2、 以'/'開頭 呼叫_routeNamed(initialRouteName, allowNull: true, arguments: null);生成Route

3、 其他 以'/'為分隔符,呼叫_routeNamed生成多個Route,依次push,適用於直接生成第n層頁面的情況

核心方法為_routeNamed,下面為部分程式碼

    final RouteSettings settings = RouteSettings(
      name: name,
      isInitialRoute: _history.isEmpty,
      arguments: arguments,
    );
    Route<T> route = widget.onGenerateRoute(settings);
複製程式碼

首先生成RouteSettings,再呼叫widget.onGenerateRoute,這個方法就是_WidgetsAppState的 _onGenerateRoute方法

 Route<dynamic> _onGenerateRoute(RouteSettings settings) {
    final String name = settings.name;
    final WidgetBuilder pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
        ? (BuildContext context) => widget.home
        : widget.routes[name];

    if (pageContentBuilder != null) {
      assert(widget.pageRouteBuilder != null,
        'The default onGenerateRoute handler for WidgetsApp must have a '
        'pageRouteBuilder set if the home or routes properties are set.');
      final Route<dynamic> route = widget.pageRouteBuilder<dynamic>(
        settings,
        pageContentBuilder,
      );
      assert(route != null,
        'The pageRouteBuilder for WidgetsApp must return a valid non-null Route.');
      return route;
    }
    if (widget.onGenerateRoute != null)
      return widget.onGenerateRoute(settings);
    return null;
  }
複製程式碼

這裡先判斷是不是為Navigator.defaultRouteName且widget.home不為空,如果是Navigator.defaultRouteName就說明目標是初始化路由,然後返回對應的pageContentBuilder;這裡就可以回答第一個問題了了,先判斷widget.home,如果為空的話再取initialRoute;

取到pageContentBuilder後就該進行下一步了;這裡分兩種情況:

一種是pageContentBuilder不為空: 這時呼叫widget.pageRouteBuilder,這個pageRouteBuilder在WidgetsApp例項化的時候有預設值;

pageContentBuilder為空: 呼叫widget.onGenerateRoute,即MaterialApp中所傳的onGenerateRoute 最後返回Route 到這裡Route的生成就結束了,之後回到NavigatorState的initState方法,對Route進行判斷,進行push操作或進行為空時的邏輯處理

使用例子

官方的例子已經給出了一種用法,下面是另一種方法

const MaterialApp(
      navigatorObservers: [
        MyObserver()
      ],
      navigatorKey: navigatorKey,
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: chooseFirstView(PreferenceUtil.syncGetBool('isLogin')),
      onGenerateRoute: generator,
    );
    
    
Route<dynamic> generator(RouteSettings routeSettings) {
 return MaterialPageRoute<dynamic>(
      settings: routeSettings,
      fullscreenDialog: false,
      builder: (BuildContext context) {
        Widget next = Routers.chooseRoute(routeSettings.name)(routeSettings.arguments);
        return next;
      });
}

複製程式碼

我這裡這裡根據routeSettings.name來生成對應的Widget,builder返回要顯示的Widget就可以了

關於Navigator

前面的部分已經對Navigator有了部分描述,這裡再做一些補充,Navigator的核心方法就兩個:push和pop其他的pushNamed,pushAndRemoveUntil,pushReplacement,核心都是push;具體都是幹什麼的,看下程式碼實現就好了;

Navigator的push方法如下

  static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
  }
複製程式碼

通過Navigator.of(context)來取到NavigatorState,實質上呼叫了NavigatorState的push方法,我們來看下這個of

final NavigatorState navigator = rootNavigator
        ? context.findRootAncestorStateOfType<NavigatorState>()
        : context.findAncestorStateOfType<NavigatorState>();
複製程式碼

呼叫了context的兩個方法來取得NavigatorState, BuildContext是個抽象類,具體的實現在 Element 裡面,這個關係就需要去看Widget的實現程式碼了,這裡不過多描述, 繼承關係如下: StatefulElement -> ComponentElement -> Element implements BuildContext 下面是程式碼的實現

T findAncestorStateOfType<T extends State<StatefulWidget>>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  Element ancestor = _parent;
  while (ancestor != null) {
    if (ancestor is StatefulElement && ancestor.state is T)
      break;
    ancestor = ancestor._parent;
  }
  final StatefulElement statefulAncestor = ancestor;
  return statefulAncestor?.state;
}



T findRootAncestorStateOfType<T extends State<StatefulWidget>>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  Element ancestor = _parent;
  StatefulElement statefulAncestor;
  while (ancestor != null) {
    if (ancestor is StatefulElement && ancestor.state is T)
      statefulAncestor = ancestor;
    ancestor = ancestor._parent;
  }
  return statefulAncestor?.state;
}

複製程式碼

這兩個方法的實現幾乎一模一樣,感覺是可以優化下的,核心邏輯就是找_parent一直找到對應的T,我們要找的就是NavigatorState;就是說,在Navigator.pushNamed方法中,你傳進去個context(這個必須是State的context,不能是StatelessWidget的context)就行,會一直找到NavigatorState,這個context就是用來尋找NavigatorState的,如果想進行push操作,只要你能取到NavigatorState,不傳context也可以

MaterialApp在例項化時有個引數navigatorKey,這個是GlobalKey(不理解自行Google)可以取到對應的Widget用法如下

 final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
 
 const MaterialApp( navigatorKey: navigatorKey);
 
 navigatorKey.currentState.pushNamed(routeName, arguments: arguments);
複製程式碼

這樣的話就可以不傳context了;

關於Route

Route是個抽象類繼承關係如下 CupertinoPageRoute|MaterialPageRoute|PageRouteBuilder -> PageRoute -> ModalRoute -> TransitionRoute -> OverlayRoute -> Route; 具體定義了Route的實現,操作,顯示層,動畫等等;這裡重點介紹最常用的三個;其他的暫時略過 CupertinoPageRoute和MaterialPageRoute是兩種風格的基礎Route,來看下MaterialPageRoute

MaterialPageRoute({
    @required this.builder,
    RouteSettings settings,
    this.maintainState = true,
    bool fullscreenDialog = false,
  })
複製程式碼

builder是用來生成要顯示的Widget

settings 有路由的基本資訊

maintainState 當路由為inactive狀態時,是否需要儲存路由狀態

fullscreenDialog 是否為全屏動畫(這個屬性為true,AppBar會有個關閉按鈕來代替返回鍵,在iOS端會呈現Modal的彈出方式,從下往上,並且會禁用掉左滑返回操作) CupertinoPageRoute的引數和這個是一樣的,來看下PageRouteBuilder:

PageRouteBuilder({
    RouteSettings settings,
    @required this.pageBuilder,
    this.transitionsBuilder = _defaultTransitionsBuilder,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.opaque = true,
    this.barrierDismissible = false,// dialog會用到
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
    bool fullscreenDialog = false,
  })
複製程式碼

多出來的引數,以transition開頭的是用來設定轉場動畫的效果和時長;(在Flutter中我們會用到類似showDialog()的方法,其本質也是呼叫了Navigator和Route相關的方法,其餘的引數在這些方法中有用到,這裡不多探討),我們來看個簡單的例子

Navigator.push(context, PageRouteBuilder(
              opaque: true,
              pageBuilder: (BuildContext context,Animation<double> animation,
                  Animation<double> secondaryAnimation) {
                return
                  Scaffold(
                    appBar: AppBar(title: Text('測試')),
                    body:  Center(child: Text('My PageRoute'))
                  );
              },
              transitionDuration:const Duration(seconds: 2),
              transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
                return SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(1, 0),
                    end: Offset.zero,
                  ).animate(animation),
                  child: child, // child is the value returned by pageBuilder
                );
              }
          ));
複製程式碼

transitionDuration設定了動畫時長為2seconds,轉場動畫為SlideTransition(滑動),從Offset(1, 0)到Offset.zero,下面是位置對應圖:0,0對應的是當前螢幕,例子中就是從右向左滑入當前螢幕中,動畫還有RotationTransition(旋轉),ScaleTransition(縮放)等等,可以去嘗試下

Flutter中的Navigator和Route

總結

這裡只是簡單介紹了下Navigator和Route,具體的關於Navigator的push和pop都做了哪些操作,stack是怎麼儲存的,Navigator的程式碼裡有詳細邏輯;Route的具體構成通過showDialog()來說明會比較好一些,感興趣的可以去原始碼裡看一下,有什麼不對的地方還請大佬指正

相關文章