Flutter之Navigator源码解析

3,406 阅读7分钟

Flutter中,页面的跳转是通过Navigator来实现。通过几句简单的代码就可以实现页面的跳转并传递对应的参数。那么具体实现是怎样的尼?下面就来一窥究竟。

1、根Navigator

Flutter中,一切皆WidgetNavigator也不能例外。但我们并没有主动添加Navigator,但是又可以通过Navigator来进行页面跳转,这是怎么回事尼?

我们在进行Flutter开发时,必须以MaterialAppWidgetsApp来作为第一个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;
  }

rootNavigatortrue表示获取根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对象。_historyNavigatorState中的一个数组,也是通常所说的路由栈。在跳转一个新页面时,会将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中,然后调用OverlayStateinsertAll函数。

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。

再来看OverlayStatebuild的函数,在该函数中会对需要保存在内存中的页面进行分类,主要有以下两类。

  • 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这个函数。该函数中会调用NavigatorStatefinalizeRoute函数来释放资源。而finalizeRoute又会调用routedispose函数。

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(替换当前页面)等。但这些函数的具体实现跟pushpop的实现基本上一致,所以只要理解了flutter中如何进行页面跳转,那么也就基本上了解了Navigator的的实现原理。也就知道了弹窗的打开、关闭的实现原理,因为这些操作也是通过Navigator来实现的。

【参考资料】

Flutter进阶:路由、路由栈详解及案例分析

Flutter 路由原理解析