Flutter中的导航和路由简析

开始

无论app还是webapp,路由都是必不可少的,相对于webapp,app的路由一般都更加强大和可控,这方面web实在太欠缺,而Flutter很明显完全克服了web的缺点,拥有一个更为完善的路由模块,这也是Flutter整个框架的特点,吸收web开发优点,但也克服web那些显而易见的缺点,提供一个更为轻松高效的开发环境,好吧,接下来一起深入了解这个模块吧。

从哪里返回

可以理解Flutter仅仅提供一个View层,其实相当多的功能都要依赖原生,例如电池信息,位置信息,网络信息等等,更为简单的说Flutter就是Super WebView。
同样当我们按下返回键时,就需要原生层告诉Flutter,弹出一个路由,让它返回上一级页面。
来到WidgetsBinding.initInstances方法:

void initInstances() {
    ...
    SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
    ...
  }

接着_handleNavigationInvocation方法:

Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) {
    switch (methodCall.method) {
      case 'popRoute':
        return handlePopRoute();
      case 'pushRoute':
        return handlePushRoute(methodCall.arguments);
    }
    return new Future<Null>.value();
  }

根据原生层的调用选择弹出一个路由或者压入一个路由,现在主要追踪弹出路由的处理。
接着handlePopRoute方法:

Future<Null> handlePopRoute() async {
    for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
      if (await observer.didPopRoute())
        return;
    }
    SystemNavigator.pop();
  }

这里也是一个观察者模式的实现了,通知所有监听者弹出路由,这里主要一个处理就是如果didPopRoute返回false(也就是没有路由愿意处理)就交给系统默认处理,一般要么退出这个Activity,要么退出应用。
那么在哪里注册这个监听器并与Navigator组件关联起来的尼?
答案就在_WidgetsAppState这类里面:

void initState() {
    ...
    WidgetsBinding.instance.addObserver(this);
  }

再看一下这个类的didPopRoute和didPushRoute方法:

// On Android: the user has pressed the back button.
  @override
  Future<bool> didPopRoute() async {
    final NavigatorState navigator = _navigator.currentState;
    return await navigator.maybePop();
  }

  @override
  Future<bool> didPushRoute(String route) async {
    final NavigatorState navigator = _navigator.currentState;
    navigator.pushNamed(route);
    return true;
  }

在这里就把Navigator关联起来了。

Navigator和Route

Navigator的职责是负责管理Route的,管理方式就是利用一个栈不停压入弹出,当然也可以直接替换其中某一个Route。而Route作为一个管理单元,主要负责创建对应的界面,响应Navigator压入路由和弹出路由。
Flutter定义路由的方式跟前端MVC框架是很相似的,你会看到有这种类似的:/home,/posts,/posts/:id等等,搞前端的同学应该想到熟悉。

弹出路由

接着继续追踪路由弹出处理流程,看一下NavigatorState.maybePop方法:

Future<bool> maybePop([dynamic result]) async {
    final Route<dynamic> route = _history.last;
    assert(route._navigator == this);
    final RoutePopDisposition disposition = await route.willPop();
    if (disposition != RoutePopDisposition.bubble && mounted) {
      if (disposition == RoutePopDisposition.pop)
        pop(result);
      return true;
    }
    return false;
  }

这个时候,Navigator会询问Route是否要自己处理还是交给系统处理,当Route.willPop返回值为RoutePopDisposition.bubble时即交给系统处理,这里也简单介绍RoutePopDisposition三个枚举值:

  • pop 弹出路由,正常情况返回上一级

  • doNotPop 不弹出,沉默处理,很多时候出现在一些表单填写的情况,必须完成页面内容,或者提示用户点击第二次才能退出

  • bubble 交给系统处理,一般直接退出应用

再看一下Route.willPop默认实现:

Future<RoutePopDisposition> willPop() async {
    return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
  }

先会判断自身是否是最后一个路由,如果是交给系统处理退出应用,如果不是弹出一个路由,很正常的行为实现。
所以当返回的是pop时,调用Navigator.pop方法:

bool pop([dynamic result]) {
    final Route<dynamic> route = _history.last;
    bool debugPredictedWouldPop;
    if (route.didPop(result ?? route.currentResult)) {
      if (_history.length > 1) {
        setState(() {
          _history.removeLast();
          if (route._navigator != null)
            _poppedRoutes.add(route);
          _history.last.didPopNext(route);
          for (NavigatorObserver observer in widget.observers)
            observer.didPop(route, _history.last);
        });
      } else {
        return false;
      }
    } else {
      assert(!debugPredictedWouldPop);
    }
    _cancelActivePointers();
    return true;
  }

在弹出路由前,会调用Route.didPop方法,也可以看到就算之前Route.willPop返回值为pop,仍然可以在Route.didPop返回false改变这个行为,从而不弹出路由。
但是如果Route.didPop方法返回的是true,就会把当前路由弹出,并调起现在当前的路由didPopNext方法通知它已经回到前台,做好一些状态恢复工作,例如:拉取最新列表信息。
而最后的_cancelActivePointers方法,立刻分发一个PointerCancel事件,这个时候手势识别器的状态会被重置,例如:双击手势,刚点了第一下就按了返回键,就会重置状态。

接着Route.didPop方法,当我们退出一个页面的时候一般都会执行一个过渡动画,但是过渡动画的持续时间多少,Navigator无法知道,所以Route要自己负责调起NavigatorState.finalizeRoute方法,通知Navigator释放路由,然后Navigator会回调Route.dispose方法释放Route自身资源,路由的生命周期结束。

压入路由

直接来到NavigatorState.pushNamed方法:

Future<dynamic> pushNamed(String name) {
    return push(_routeNamed(name));
}

再跳到_routeNamed方法:

Route<dynamic> _routeNamed(String name, { bool allowNull: false }) {
    final RouteSettings settings = new RouteSettings(
      name: name,
      isInitialRoute: _history.isEmpty,
    );
    Route<dynamic> route = widget.onGenerateRoute(settings);
    if (route == null && !allowNull) {
      route = widget.onUnknownRoute(settings);
    }
    return route;
  }

也很简单根据名称查找路由,可以看看MaterialApp的实现:

Route<dynamic> _onGenerateRoute(RouteSettings settings) {
    final String name = settings.name;
    WidgetBuilder builder;
    if (name == Navigator.defaultRouteName && widget.home != null)
      builder = (BuildContext context) => widget.home;
    else
      builder = widget.routes[name];
    if (builder != null) {
      return new MaterialPageRoute<dynamic>(
        builder: builder,
        settings: settings,
      );
    }
    if (widget.onGenerateRoute != null)
      return widget.onGenerateRoute(settings);
    return null;
  }

我们创建MaterialApp时都会传入一个route的map,然后就是根据map来创建route,就这么简单。
如果找到路由,就像我们访问网页404,应该给一个友好的页面告诉用户不存在,就可以在onUnknownRoute回调中返回一个页面。

接着NavigatorState.push方法:

Future<dynamic> push(Route<dynamic> route) {
    setState(() {
      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);
      for (NavigatorObserver observer in widget.observers)
        observer.didPush(route, oldRoute);
    });
    _cancelActivePointers();
    return route.popped;
  }

重点看Route.install方法,首先了解一下Route的继承关系:

clipboard.png

可以看到Route的实现都继承自OverlayRoute,而OverlayRoute.install的实现:

void install(OverlayEntry insertionPoint) {
    assert(_overlayEntries.isEmpty);
    _overlayEntries.addAll(createOverlayEntries());
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
    super.install(insertionPoint);
  }

navigator会把Route.createOverlayEntries创建的OverlayEntries添加到自己的Overlay组件上;而createOverlayEntries方法干了啥尼,再来到ModalRoute.createOverlayEntries:

Iterable<OverlayEntry> createOverlayEntries() sync* {
    yield new OverlayEntry(builder: _buildModalBarrier);
    yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }

可以看到构建两个OverlayEntry,我们经常看到对话框后面还有一层遮罩,就是由这里产生的。

再看一下OverlayEntry有两个重要的属性opaque和maintainState,当我们把OverlayEntry添加到Navigator的Overlay组件时,Overlay组件构建过程处理是这样的:

Widget build(BuildContext context) {
    // These lists are filled backwards. For the offstage children that
    // does not matter since they aren't rendered, but for the onstage
    // children we reverse the list below before adding it to the tree.
    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(new _OverlayEntry(entry));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        offstageChildren.add(new TickerMode(enabled: false, child: new _OverlayEntry(entry)));
      }
    }
    return new _Theatre(
      onstage: new Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      offstage: offstageChildren,
    );
  }

当有一个OverlayEntry的opaque为true时(就是不透明看不到下面的页面),默认情况下在它之下OverlayEntry不会实例化(也没必要),但是如果设置maintainState为true时,OverlayEntry会build出组件树,但是这些组件不会被布局和绘制,主要用于维持组件状态。

当Route.dipose方法调起后,Route的OverlayEntry才会从Navigator的Overlay组件移除。

再回头当Route.install方法调起后,一般过渡动画会在这里构建,接会调用Route.didPush方法,过渡动画会在这里播放,最后再调用前一个路由的didChangeNext方法通知它被退到后台,可以在这个方法里保存自己状态信息,等下次回到前台时恢复。

值得注意的地方

在我们初始化的时候,我们默认初始化路由是:‘/’,但是我们如果初始化路由是这样的:‘/posts/123’,框架是怎样处理的尼?
我们来到NavigatorState.initState方法

void initState() {
    super.initState();
    for (NavigatorObserver observer in widget.observers) {
      assert(observer.navigator == null);
      observer._navigator = this;
    }
    String initialRouteName = widget.initialRoute ?? Navigator.defaultRouteName;
    if (initialRouteName.startsWith('/') && initialRouteName.length > 1) {
      initialRouteName = initialRouteName.substring(1); // strip leading '/'
      assert(Navigator.defaultRouteName == '/');
      final List<String> plannedInitialRouteNames = <String>[
        Navigator.defaultRouteName,
      ];
      final List<Route<dynamic>> plannedInitialRoutes = <Route<dynamic>>[
        _routeNamed(Navigator.defaultRouteName, allowNull: true),
      ];
      final List<String> routeParts = initialRouteName.split('/');
      if (initialRouteName.isNotEmpty) {
        String routeName = '';
        for (String part in routeParts) {
          routeName += '/$part';
          plannedInitialRouteNames.add(routeName);
          plannedInitialRoutes.add(_routeNamed(routeName, allowNull: true));
        }
      }
      if (plannedInitialRoutes.contains(null)) {
        push(_routeNamed(Navigator.defaultRouteName));
      } else {
        for (Route<dynamic> route in plannedInitialRoutes)
          push(route); //连续压入
      }
    } else {
      Route<dynamic> route;
      if (initialRouteName != Navigator.defaultRouteName)
        route = _routeNamed(initialRouteName, allowNull: true);
      if (route == null)
        route = _routeNamed(Navigator.defaultRouteName);
      push(route);
    }
    for (Route<dynamic> route in _history)
      _initialOverlayEntries.addAll(route.overlayEntries);
  }

整个处理也很简单,直接把/posts和/posts/123压入路由栈中,而不是仅仅只是把/posts/123压进去,这跟web的url跳转就有点出入了,值得注意。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值