Flutter命名路由获取传参的两种方式

  • 概述

    普通路由传递参数采用的是硬传递的方式:

    Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) {
                    return TipRoute(
                      // 路由参数
                      text: "我是提示xxxx",
                    );
                  },
                ),
              );k
    

    可以看到,参数通过新页面组件的构造方法直接传入的。

    但是标准的做法通常是建立一个路由表,通过字符串名字映射路由信息,因此Flutter提供了一种命名路由方式:

    Future pushNamed(BuildContext context, String routeName,{Object arguments})
    

    我们本文就是整理分析关于它传值后获取它的两种途径。

  • 路由映射表

    不管哪种方式,首先我们都需要定义路由映射表,比如:

    static final routes = {
      HOME_PAGE: (context) => MyHomeWidget('Flutter Demo Home Page'),
      MIX_DEMO: (context,{argument}) => MixDemo(title: argument),
    };
    
  • 方式一:通过onGenerateRoute函数

    这种方式首先在MaterialApp中不能指定routes属性,因为onGenerateRoute函数只有在routes不存在或者在其中找不到路由的时候才会调用,具体原理之前有博文介绍过,此处不再赘述。

    这种方式要求映射表不指定routes的类型,虽然按照MaterialApp的routes类型来看,我们应该定义成:

    Map? routes;
    typedef WidgetBuilder = Widget Function(BuildContext context);
    

    但是如果使用onGenerateRoute方式的话,定义成这样是不行的,为什么,接着往下看。

    下面我们看onGenerateRoute中该如何写:

    ///针对命名路由,跳转前拦截操作,找不到路由的时候才会走这里
    Route? _myGenerateRouteLogic(RouteSettings settings) {
      Function? pageContentBuilder = RouteManager.routes[settings.name];
      if (pageContentBuilder != null) {
        if (settings.arguments != null && settings.arguments is String) {
          var route = MaterialPageRoute(
              builder: (context) => pageContentBuilder(context,
                  argument: settings.arguments as String));
          return route;
        } else {
          var route = MaterialPageRoute(
              builder: (context) => pageContentBuilder(context));
          return route;
        }
      }
    }
    

    可见,这里会从映射表中按照路由的名字找到对应的pageContentBuilder,这里会用Function接收,这里也能理解,如果定义成WidgetBuilder的话,这里的实际类型就会是WidgetBuilder类型,而我们这里会根据arguments是否为空来调用不同参数的pageContentBuilder方法,使用WidgetBuilder接收是编译不过去的。

    现在获取的pageContentBuilder是Widget Function(dynamic)类型,它可以同时应对带参数的和不带参数的Function,所以可以通过编译。

    这种方式的映射表需要根据需求添加可选参数,必须是可选参数,因为实际上函数类型还是限制成和WidgetBuilder一样的,只不过是参数类型用dynamic适配的。

    现在我们来梳理一下逻辑,MaterialApp中不设置routes属性,或者routes指定的路由表中不配置传参路由页面(最好不要这么做,显得怪怪的),这样以来路由跳转时就会走到onGenerateRoute函数中,在这里通过RouteSettings的name去路由表(这个路由表中必须得有获取参数的路由页面了)中找到生成该路由的pageContentBuilder,判断RouteSettings的arguments,从而适配带参数和不带参数的pageContentBuilder调用,在pageContentBuilder中把可选参数通过路由页面的构造方法传入。

    可见,这种方式最终还是通过路由页面的构造函数直接传入的。

  • 方式二:通过ModalRoute

    第一种方式可以说是另辟蹊径,因为onGenerateRoute的初衷是为了处理一些路由表中未找到路由的情况的,用它来实现传递参数有一些驴唇不对马嘴了,相比于第一种方式,第二种方式就优雅很多了。

    在路由页面的build中,我们直接调用ModalRoute.of(context)?.settings.arguments就可以拿到命名路由传过来的参数,代码越少,原理越绕...那它的原理是什么呢?

    @optionalTypeArgs
    static ModalRoute? of(BuildContext context) {
      final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
      return widget?.route as ModalRoute?;
    }
    

    可以看到,这里会在组件树中通过找到父级中最近的一个_ModalScopeStatus,然后拿到它的route,我们来找一下 _ModalScopeStatus是什么。

    最终找到的是:

    @override
    Iterable createOverlayEntries() sync* {
      yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
      yield _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
    }
    

    _buildModalScope函数中最终构造的子树中含有 _ModalScopeStatus,createOverlayEntries这个方法是在NavigatorState的 _push流程中会调用的方法,可以理解成建立新路由的面板。

    Widget _buildModalScope(BuildContext context) {
      // To be sorted before the _modalBarrier.
      return _modalScopeCache ??= Semantics(
        sortKey: const OrdinalSortKey(0.0),
        child: _ModalScope(
          key: _scopeKey,
          route: this,
          // _ModalScope calls buildTransitions() and buildChild(), defined above
        ),
      );
    }
    

    _ModalScope设置了一个route属性,指定为当前类, _buildModalScope所在的类正是ModalRoute,它的RouteSettings属性正是前面pushNamed方法一步步传入的,我们来看看RouteSettings在怎么生成的。

    @optionalTypeArgs
    Future pushNamed(
      String routeName, {
      Object? arguments,
    }) {
      return push(_routeNamed(routeName, arguments: arguments)!);
    }
    

    很明显,Route由_routeNamed生成,它的核心代码如下:

    Route? _routeNamed(String name, { required Object? arguments, bool allowNull = false }) {
      if (allowNull && widget.onGenerateRoute == null)
        return null;
     
      final RouteSettings settings = RouteSettings(
        name: name,
        arguments: arguments,
      );
      Route? route = widget.onGenerateRoute!(settings) as Route?;
      if (route == null && !allowNull) {
        //如果没找到返回onUnknownRoute生成的404路由
        route = widget.onUnknownRoute!(settings) as Route?;
      }
      return route;
    }
    

    可以看到,RouteSettings是在这里创建的,它的arguments正是我们调用pushNamed方法时传入的arguments属性的值,而Route通过widget.onGenerateRoute生成,因为_routeNamed方法在NavigatorState中,所以widget自然是Navigator,Navigator的widget.onGenerateRoute也是构造时传入的,我们这里是App内的跳转,所以找到 _WidgetsAppState中的build中Navigator的构造处,发现这里的onGenerateRoute属性设置的是一个叫 _onGenerateRoute的函数:

    Route? _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 route = widget.pageRouteBuilder!(
          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;
    }
    

    可以看到,这里会通过name去App指定的routes映射表中取pageContentBuilder,route又通过widget.pageRouteBuilder产生,找到_MaterialAppState的 _buildWidgetApp中发现pageRouteBuilder是:

    pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) {
      return MaterialPageRoute(settings: settings, builder: builder);
    },
    

    可以看到这里和我们第一种方式的onGenerateRoute中的构造route的方式差不多,只不过这里的MaterialPageRoute会把RouteSettings传进去,而我们第一种方式不用传是因为我们那时是直接通过路由页面的构造函数传入参数的,而这里会把settings传入是因为之后是通过route找到settings后再取参数。

    而MaterialPageRoute最终继承自ModalRoute,所以dependOnInheritedWidgetOfExactType会取到route从而取到settings。

  • 总结

    关于路由参数的获取我们这里整理分析了两种方式,第一种属于旁门招数,而第二种也是官方推荐使用的方式,它通过把参数保存在组件树的当前Route中,然后通过dependOnInheritedWidgetOfExactType获取最近的Route持有者的方式来获取参数。

你可能感兴趣的:(Flutter命名路由获取传参的两种方式)