Flutter中的Navigator和Route

3,479 阅读7分钟

导航和路由,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(缩放)等等,可以去尝试下

总结

这里只是简单介绍了下Navigator和Route,具体的关于Navigator的push和pop都做了哪些操作,stack是怎么存储的,Navigator的代码里有详细逻辑;Route的具体构成通过showDialog()来说明会比较好一些,感兴趣的可以去源码里看一下,有什么不对的地方还请大佬指正