Flutter命名路由及传参的深度实践与解读

Flutter命名路由及传参的深度实践与解读

在写Flutter应用时,实现页面间交叉跳转时,通常多是使用命名路由,这样更方便。但是经常会遇到某些情况不能达到理想的效果,网罗众文基本都千篇一律,大多数是源自Flutter中文官网或几个前期博文的加工再发,没有针对Flutter的命名路由特性进行深度的分析。本人在近期项目需求中遇到了路由问题,顺便对Flutter命名路由做了深度实践,记录发文,希望对后来者有所帮助!
.

本次深入实践的起因

在我们的一个项目中有个页面暂称为“Page4”,会有不同的页面在不同的路由位置跳转到这个页面,但是需要在这个页面点确定按钮时,跳转到当前路由的前N个页面,例如是“Page2”(Page4是经过Page2从首页一路加载过来的),并刷“Page2”。然后要求在“Page2”执行pop方法应该可以返回到“Page1”页面。

期望流程及路由记录如下:

  1. 从首页进入到Page4:Home → Page1 → Page2→ Page3 → Page4
  2. 在Page4点确定按钮:Home → Page1 → Page2(新创建的)
  3. 在Page2点返回按钮:Home → Page1

在Flutter的路由方法里,我们发现Navigator.pushNamedAndRemoveUntil方法携带ModalRoute.withName参数,刚好是我们需要的。这个方法携带该参数后,是从最近向前删除路由到指定的路由为止(不是清空),然后再创建指定的页面(新创就等于刷新了)。然后我们在使用时,发现跳转没问题,跳转后再点返回出现了黑屏,网罗了一大堆原因均不吻合。后又遇到命名路由传参问题,经仔细实践,更深入了解路由的原理,最终一并得以解决。

虽然有其他实现方法可以达到目的,我们不在这里讨论,在这只探讨Flutter命名路由的深入实践过程及得出的结果!
.

跳转后点返回黑屏或退出应用的BUG再现

BUG示例代码:

main.dart

import 'package:flutter/material.dart';
import 'home.dart';
import 'routes.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '演示命名路由使用',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Home(),
      routes: routes,
      onGenerateRoute: onGenerateRoute,
    );
  }
}

.

home.dart

import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  Home({Key key}) : super(key: key);
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('命名路由示例'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Flutter 欢迎您!'),
            RaisedButton(
              child: Text("pushNamed跳page01页"),
              onPressed: () {
                Navigator.pushNamed(context, '/page01', arguments: '这是来自Home页的数据');
              },
            ),
          ],
        ),
      ),
    );
  }
}

.

page01.dart

import 'package:flutter/material.dart';

class Page01 extends StatefulWidget {
  Page01({Key key, this.param}) : super(key: key);
  final String param;
  @override
  _Page01State createState() => _Page01State(msg:this.param);
}

class _Page01State extends State<Page01> {
  _Page01State({this.msg});
  String msg;

  @override
  void initState() {
    print('类初始化收到:' + (msg ?? '空'));     // 控制台输出msg参数,监测传值变化
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    String args = ModalRoute.of(context).settings.arguments;
    print('settings收到:' + (args ?? '空'));    // 控制台输出args参数,监测传值变化
    return Scaffold(
      appBar: AppBar(title: Text('页面 page01')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('类初始化收到:' + (msg ?? '空')),
            Text('settings收到:' + (args ?? '空')),
            RaisedButton(
              child: Text("pushNamed跳page02页"),
              onPressed: () {
                Navigator.pushNamed(context, '/page02', arguments: '这是来自page01页的数据');
              },
            ),
          ],
        ),
      ),
    );
  }
}

.

page02.dart、page03.dart
与page01.dart页代码基本一样,仅标题和传递参数略有差别,这里不再复制

.

page04.dart

import 'package:flutter/material.dart';

class Page04 extends StatefulWidget {
  Page04({Key key, this.param}) : super(key: key);
  final String param;
  @override
  _Page04State createState() => _Page04State(msg:this.param);
}

class _Page04State extends State<Page04> {
  _Page04State({this.msg});
  String msg;

  @override
  void initState() {
    print('类初始化收到:' + (msg ?? '空'));     // 控制台输出msg参数,监测传值变化
    super.initState();
  }
  
  @override
  Widget build(BuildContext context) {
    String args = ModalRoute.of(context).settings.arguments;
    print('settings收到:'+(args ?? '空'));    // 控制台输出args参数,监测传值变化
    return Scaffold(
      appBar: AppBar(title: Text('页面 page04')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('类初始化收到:' + (msg ?? '空')),
            Text('settings收到:' + (args ?? '空')),
            RaisedButton(     // A跳转按钮
              child: Text("保留路由新建page01页"),
              onPressed: () {
                Navigator.pushNamed(context, '/page01', arguments:'pushNamed');
              },
            ),
            RaisedButton(     // B跳转按钮
              child: Text("删部分路由后新建page01页"),
              onPressed: () {
                Navigator.pushNamedAndRemoveUntil(context, '/page01', ModalRoute.withName('/home'), arguments:'pushNamedAndRemoveUntil');
              },
            ),
          ],
        ),
      ),
    );
  }
}

.

routes.dart

import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';


final routes = {
  '/home': (context) => Home(),
  '/page01': (context, {arguments}) => Page01(param:arguments),
  '/page02': (context, {arguments}) => Page02(param:arguments),
  '/page03': (context, {arguments}) => Page03(param:arguments),
  '/page04': (context, {arguments}) => Page04(param:arguments),
};

Route<dynamic> onGenerateRoute(RouteSettings settings) {
  String routeName = settings.name;
  print('当前访问路由名:$routeName');
  final Function pageContentBuilder = routes[routeName];
  if (pageContentBuilder != null) {
    if (settings.arguments != null) {
      final Route route = MaterialPageRoute(
        builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
      );
      return route;
    } else {
      final Route route = MaterialPageRoute(
        builder: (context) => pageContentBuilder(context),
      );
      return route;
    }
  }
  return null;
}

.

执行pop黑屏或退出应用的原由

从首页home页面,首次一路点击按钮过来,在page04当前页时,路由栈内顺序应该是:
[’/home’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’]
但实际上里面没有存在 ‘/home’,因为创建 home 页面时没有使用命名路由,所以路由栈内的名字是:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’],’/'是路由管理器默认给的初始化名字(具体名字不一定是这个)

page04.dartA跳转按钮 事件中,我们使用的是 pushNamed 方法,实现的结果与我们预期的是一致的。也就是之前路由中有一个 page02 页面,在当前 page04 页面之后再新建一个 page02(因为要刷新,所以直接使用了新建),这样返回的话需要点5次返回才到首页,而期望是返回2次即到首页。所以尝试使用 B跳转按钮 的事件。
点 A跳转按钮 后,路由栈内是:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’]

page04.dartB跳转按钮 事件中,我们使用的是 pushNamedAndRemoveUntil 方法,期望清掉路由栈中的 ‘page02’, ‘page03’, ‘page04’,然后创建一个新的 page02。
点 B跳转按钮 后期望路由栈内是:
[’/home’, ‘/page01’, ‘/page02’]
但当前代码结果实际不是,经过实践以为找不到 ‘/home’ 和 ‘/page01’ ,则把路由栈全清空了,实际是:
[’/page02’]
也就是只有 page02 页面了,没有上一页了,所以这是调用pop方法返回上一页,找不到 page01了,这是黑屏或退出现象的主因。
.

实现类初始化接收不到参数的原由

经过测试发现,如果使用上述命名路由的方式,虽然在路由Map中,page01-04都配置了参数传递,但是相应页面中的实现类初始化接参都是空的。经过反复实践,发现原因出在以下几点:

  1. 路由Map中的“(context, {arguments}) => Page01(param:arguments)”这种写法要配合路由拦截器才有效,否则传递不过去参数。
  2. main.dart 中 MaterialApp 配置了 routes 属性并指向了 路由Map(routes.dart中的routes),这样只要在routes中能找到的页面名字,就不会触发路由拦截器,因此参数传递也就不可能有效。
    .

深度实践整理后的解读

.

深度实践后的Flutter命名路由示例代码

下面是示例应用截图:
在这里插入图片描述
.
代码中关键位置标有详细注释,这均是经过实践得出的结论,若有错误请以官方文档为准。

main.dart

import 'package:flutter/material.dart';
import 'routes.dart';


void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '演示命名路由使用',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      //home: Home(),
      // 应用初始化时第一个要加载的路由,通常称为根路由,不使用这个配置会造成查不到根路由页面的名称
      //initialRoute: '/',

      // 命名路由Map,创建页面是这里有的就不会触发onGenerateRoute事件,不使用则直接触发onGenerateRoute事件
      //routes: routes,

      // 路由拦截器,产生新路由页面时,如果在路由列表routes中查不到,则会触发这个事件
      // 所以没有给routes属性赋值的话,则执行任何一个命名路由相关的创建页面方法都会触发本事件
      onGenerateRoute: onGenerateRoute,
    );
  }
}

.

home.dart

import 'package:flutter/material.dart';


class Home extends StatefulWidget {
  Home({Key key}) : super(key: key);

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('命名路由示例-首页'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Flutter 欢迎您!'),
            RaisedButton(
              child: Text("pushNamed创建page01页"),
              onPressed: () {
                Navigator.pushNamed(context, '/page01', arguments: '这是来自Home页的数据');
              },
            ),
          ],
        ),
      ),
    );
  }
}

.

page01.dart

import 'package:flutter/material.dart';


class Page01 extends StatefulWidget {

  // 初始化时接收创建类时传递的参数,保存在param
  Page01({Key key, this.param}) : super(key: key);

  // 这个一定要是终极类型,因为本类直接继承自StatefulWidget(被标记为@immutable)
  // 为适用命名路由表,这里只设一个参数,需要多个可以传对象或Map
  // 名字不是必须为arguments,但是要与路由配置中参数名称一致,见routes.dart的'/page01'一行
  final String param;

  // 为_Page01State类中可以在build外方便获取param,这里传递this.param给_Page01State的msg
  @override
  _Page01State createState() => _Page01State(msg:this.param);
}

class _Page01State extends State<Page01> {

  // 初始化获取Page01传递过来的param给本类的msg
  _Page01State({this.msg});

  // 这里可以不是终极类型
  String msg = '空';

  @override
  void initState() {

    // 控制台输出msg参数,监测传值变化
    print('Page01类初始化收到:$msg');
    super.initState();
  }
  @override
  Widget build(BuildContext context) {

    // 通过ModalRoute.of(context).settings.arguments获取的参数,仅在build内可以
    String args = ModalRoute.of(context).settings.arguments;
    if (args == null) args = '空';

    // 控制台输出args参数,监测传值变化
    print('page01页settings收到:$args');
    return Scaffold(
      appBar: AppBar(

        // 标记是第几个页面,便于测试识别
        title: Text('页面 page01'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            // 页面输出类初始化收到的参数情况,识别更直观
            Text('类初始化收到:$msg'),

            // 页面输出类在build内使用ModalRoute的settings收到的参数情况,识别更直观
            Text('settings收到:$args'),
            RaisedButton(
              child: Text("pushNamed创建page02页"),
              onPressed: () {

                // 通过pushNamed方法跳转到page02,并传参“这是来自page01页的数据”
                Navigator.pushNamed(context, '/page02', arguments: '这是来自page01页的数据');
              },
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: () {

          // 通过popUntil方法直接返回首页home,不传参,便于测试
          // 此方法指定的页面名称必需在当前路由表中存在,否则会黑屏(例如之前有过清空路由操作)
          Navigator.popUntil(context, ModalRoute.withName('/'));
        },
        child: Text('首页'),
      ),
    );
  }
}

page02.dart、page03.dart
与page01.dart代码基本相同,只是传参与描述与页面名称相关的更改一下即可,这里就不复制

.

page04.dart

import 'package:flutter/material.dart';


class Page04 extends StatefulWidget {

  // 初始化时接收创建类时传递的参数,保存在param
  Page04({Key key, this.param}) : super(key: key);

  // 这个一定要是终极类型,因为本类直接继承自StatefulWidget(被标记为@immutable)
  // 为适用命名路由表,这里只设一个参数,需要多个可以传对象或Map
  // 名字不是必须为arguments,但是要与路由配置中参数名称一致,见routes.dart的'/page01'一行
  final String param;

  // 为_Page01State类中可以在build外方便获取param,这里传递this.param给_Page01State的msg
  @override
  _Page04State createState() => _Page04State(msg:this.param);
}

class _Page04State extends State<Page04> {

  // 初始化获取Page01传递过来的param给本类的msg
  _Page04State({this.msg});

  // 这里可以不是终极类型
  String msg = '空';

  @override
  void initState() {

    // 控制台输出msg参数,监测传值变化
    print('Page04类初始化收到:$msg');
    super.initState();
  }

  @override
  Widget build(BuildContext context) {

    // 通过ModalRoute.of(context).settings.arguments获取的参数,仅在build内可以
    String args = ModalRoute.of(context).settings.arguments;
    if (args == null) args = '空';

    // 控制台输出args参数,监测传值变化
    print('page02页settings收到:$args');
    return Scaffold(
      appBar: AppBar(

        // 标记是第几个页面,便于测试识别
        title: Text('页面 page04'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            // 页面输出类初始化收到的参数情况,识别更直观
            Text('类初始化收到:$msg'),

            // 页面输出类在build内使用ModalRoute的settings收到的参数情况,识别更直观
            Text('settings收到:$args'),

            RaisedButton(
              child: Text("A:保留路由新建page02页"),
              onPressed: () {

                // 通过pushNamed方法创建page02,并传参方法名
                // 此方法保存原路由栈,新创建一个page02页面,并压入路由栈
                // 执行后,路由栈内应该是['/home', '/page01', '/page02', '/page03', '/page04', '/page02']
                Navigator.pushNamed(context, '/page02', arguments:'pushNamed');
              },
            ),
            RaisedButton(
              child: Text("B:删部分路由后新建page02页"),
              onPressed: () {

                // 通过pushNamedAndRemoveUntil方法带ModalRoute.withName配置创建page02,并传参方法名
                // 此方法将从路由栈顶部(最近)开始逐一弹出(删除),直到遇到'/home'停止删除('/home'不会被删除),再创建page02
                // 执行后,路由栈里就只剩下了['/home', '/page01', '/page02'],page02的上一页就是page01,而不是当前页page04
                Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'pushNamedAndRemoveUntil');
              },
            ),
            RaisedButton(
              child: Text("C:清空路由栈后新建page02页"),
              onPressed: () {

                // 通过pushNamedAndRemoveUntil带(Route<dynamic> route)=>false配置创建page02,并传参方法名
                // 此方法将清空路由栈(弹出/删除所有路由),然后再创建page02
                // 执行后,路由栈里就只有['/page02'],page02变成了根页面,没有上一页了
                Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'pushNamedAndRemoveUntil');
              },
            ),
            RaisedButton(
              child: Text("D:替换当前路由无转场效果新建page02页"),
              onPressed: () {

                // 通过pushReplacementNamed方法跳转到page02,并传参方法名
                // 此方法将在原路由栈替换掉当前的路由(page04),然后再创建page02
                // 执行后,路由栈里是['/home', '/page01', '/page02', '/page03', '/page02'],page02的上一页就是page03,而不是当前页page04
                Navigator.pushReplacementNamed(context, '/page02', arguments:'pushReplacementNamed');
              },
            ),
            RaisedButton(
              child: Text("E:替换当前路由有转场效果新建page02页"),
              onPressed: () {

                // 通过popAndPushNamed方法跳转到page02,并传参方法名
                // 此方法与pushReplacementNamed方法最终在路由栈里的结果相同
                // 只是执行过程中,此方法有转场效果,而pushReplacementNamed方法没有
                Navigator.popAndPushNamed(context, '/page02', arguments:'pushReplacementNamed');
              },
            ),
            RaisedButton(
              child: Text("F:删除部分路由回到page02页"),
              onPressed: () {

                // 通过popUntil方法返回到page02,此方法无法传参,也不触发路由拦截器
                // 此方法与pushNamedAndRemoveUntil方法最终在路由栈里的结果相同
                // 只是page02不是新建的,不刷新页面保持原有状态,等同于连续的pop;而pushNamedAndRemoveUntil是新建页面,类似于刷新了页面
                // 此方法指定的页面名称必需在当前路由表中存在,否则会黑屏(例如之前有过清空路由操作)
                Navigator.popUntil(context, ModalRoute.withName('/page02'));
              },
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: () {

          // 通过popUntil方法直接返回首页home,不传参,便于测试
          // 此方法指定的页面名称必需在当前路由表中存在,否则会黑屏(例如之前有过清空路由操作)
          Navigator.popUntil(context, ModalRoute.withName('/'));
        },
        child: Text('首页'),
      ),
    );
  }
}

routes.dart

import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';


final routes = {
  // 如果标注'/',而main.dart中又没有指定home页,但指定routes为本Map,则应用初始化时自动加载'/'指定的页面
  // 若是main.dart中指定了home页,指定routes为本Map,这里就不要有'/',否则会报警告
  '/': (context) => Home(),

  // 这里的“arguments”必须是这个名字,否则取不到通过“pushNamed”等方法传递的参数
  // 这里的“param”是与对应的类中的接收参数命名对应的,不是一定要使用“arguments”这个名字
  // 这里的参数传递仅对使用onGenerateRoute路由拦截器,并在拦截器内写入参数传递的代码并触发了拦截器时才有效
  // 这里的参数传递是传递给类初始化的参数,与ModalRoute.of(context).settings.arguments无关
  //
  // 若不需要给类初始化传参,可以写成 '/page01': (context) => Page01()这样
  // 同样可以用ModalRoute.of(context).settings.arguments接收“pushNamed”等方法传递的参数
  '/page01': (context, {arguments}) => Page01(param:arguments),
  '/page02': (context, {arguments}) => Page02(param:arguments),
  '/page03': (context, {arguments}) => Page03(param:arguments),
  '/page04': (context, {arguments}) => Page04(param:arguments),
};

// 路由拦截器,只有在MaterialApp的routes属性没有赋值的情况下,才会每次创建页面都触发
// 如果指定了routes属性,那只有routes中查不到的时候才会触发
Route<dynamic> onGenerateRoute(RouteSettings settings) {
  String routeName = settings.name;

  // 控制台输出当前拦截的路由名称,方便清晰的理解路由创建流畅
  print('当前访问路由名:$routeName');

  // 为了避免报错,将在命名路由列表查不到的都指向首页(根页)
  if (!routes.containsKey(routeName)) routeName = '/';

  // 获取页名对应的类
  final Function pageContentBuilder = routes[routeName];

  if (settings.arguments != null) {

    // 如果带有参数,则调用该类的时候,需要将参数传递过去
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context, arguments: settings.arguments),

      // 这一行是命名路由顺畅应用的关键,否则新创建的页面压入路由栈的将不是自己定义的名字
      // 主要是使用 settings.name 的作用,为新创建的页面命名
      settings: settings,
    );
  } else {

    // 如果没有带参数,则调用该类的时候,不需要传递参数
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context),
      settings: settings,
    );
  }
}

.

Flutter命名路由示例用到的路由方法

  1. pushNamed
    保留当前路由栈,在命名路由列表中,根据名称查找对应的页面实现类并创建该页,然后压入路由栈。
    可以携带参数(非必传),若要传参必需以arguments命名,要传多个参数可封装成对象或Map(下同)。
    调用示例:
Navigator.of(context).pushNamed("/page1", arguments:'这里是参数');
// 或
Navigator.pushNamed(context, '/page01', arguments:'这里是参数');

上面示例表示保留当前路由栈新建page01,向page01页传递字符串参数“这里是参数”五个汉字。

  1. pushNamedAndRemoveUntil
    根据条件删除当前路由栈中的路由,然后创建新页。
    ⑴ 使用 ModalRoute.withName() 参数:
    从当前路由栈顶部(最近的)开始弹出(删除)路由,直到指定的页面为止,保留指定的页面。
    调用示例:
Navigator.of(context).pushNamedAndRemoveUntil('/page02', ModalRoute.withName('/page01'), arguments:'这里是参数');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'这里是参数');

上面示例表示从当前页开始向前依次删除路由直到page01为止,保留page01,然后新建page02,向page02页传递字符串参数“这里是参数”五个汉字。
.
⑵ 使用 (Route route)=>false 参数:
将当前路由栈清空
调用示例:

Navigator.of(context).pushNamedAndRemoveUntil('/page02', (Route<dynamic> route)=>false, arguments:'这里是参数');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'这里是参数');

上面示例表示从将当前路由栈中所有路由删除,然后新建page02,向page02页传递字符串参数“这里是参数”五个汉字。

  1. pushReplacementNamed
    在当前路由栈中删除当前路由,然后创建新页。也就是使用新建页的路由替换当前路由。此方法没有页面转场的效果。
    调用示例:
Navigator.of(context).pushReplacementNamed('/page02', arguments:'这里是参数');
// 或
Navigator.pushReplacementNamed(context, '/page02', arguments:'这里是参数');

上面示例表示将当前路由栈中的当前页路由替换为新建的page02,向page02页传递字符串参数“这里是参数”五个汉字。没有转场效果

  1. popAndPushNamed
    此方法与pushReplacementNamed在路由栈中最终结果相同,只是在进入新建页面时,有转场效果。
    调用示例:
Navigator.of(context).popAndPushNamed('/page02', arguments:'这里是参数');
// 或
Navigator.popAndPushNamed(context, '/page02', arguments:'这里是参数');

上面示例表示将当前路由栈中的当前页路由替换为新建的page02,向page02页传递字符串参数“这里是参数”五个汉字。有转场效果

  1. popUntil
    回到之前曾经打开的指定页,不重建不刷新页面,保持原页面状态,删除指定页之后的所有路由,不能携带参数。
    调用示例:
Navigator.of(context).popUntil(ModalRoute.withName('/page02'));
// 或
Navigator.popUntil(context, ModalRoute.withName('/page02'));

上面示例表示回到曾经打开的page02,删除page02页之后的所有路由
.

MaterialApp路由相关属性配置要点

在MaterialApp中与本例相关的属性有 home、initialRoute、routes、onGenerateRoute 四个。

  1. home:指定应用启动后首次加载的页面,是类名,不是命名路由的页面名称。
  2. initialRoute:同 home,但是指定的是命名路由routes中的页面名,不是类名。
  3. routes:命名路由列表,是一个Map类型,页面名映射该页对应的实现类名。
  4. onGenerateRoute:命名路由拦截器,只有在routes中查不到页名时才会触发这个事件。

▲ 在 home 中指定的页面,该页在 routes 中的页面名称是不能压入路由栈的。

▲ 因 home 与 initialRoute 作用基本相同,所以不能同时使用,否则会产生冲突。

▲ 只有配置了 routes 或 启用了 onGenerateRoute 后,才可以使用 initialRoute,否则无处查找页面名会产生错误。

▲ 若配置了 routes、home 后,routes 列表中有写 ‘/’ 映射,将会产生冲突,因为命名路由规则中,’/’ 代表的是根页,也是初始化应用首先要加载的页面。所以配置了 routes 就尽量不要配置 home。

▲ 即使在 routes 列表中给实现类配置了参数,也是不能给实现类的初始化参数传递参数的。必须配置 onGenerateRoute 并在该事件的方法内实现参数传递才可以实现,否则只能在 build 内通过 ModalRoute.of(context).settings.arguments 获取传递的参数。

▲ 若希望每次通过命令路由创建页面都能走路由拦截器,那定义routes列表后,不要配置routes属性。由拦截器去加载routes列表,否则在routes列表存在新建页名的时候,路由拦截器是不会被触发的。
.

routes列表onGenerateRoute拦截器要点

通常为了方便,都是把这两个放在一个独立的文件内。

先看一下routes列表的定义,粘贴部分代码如下:

final routes = {
  '/': (context) => Home(),
  '/page01': (context) => Page01(),
  '/page02': (context, {arguments}) => Page02(param:arguments),
};

▲ 上面代码中 ‘/’ 是应用初始化时,在没有配置 initialRoute(在MaterialApp中)和 home 时自动加载的页面,若配置了initialRoute则会加载initialRoute指定的页面。所以尽量我们把应用的默认加载页命名为 ‘/’,这更利于代码阅读与交流(其他每个页名前面的 / 也不是必需的)。

▲ 上面代码中的 {arguments} 和 param:arguments 是为了给页面实现类(如:Page02)的初始化参数传递参数而写的,arguments 这个名字不能更改,param 必须与 Page02 类中的接收参数名称一致。这个配置要最终实现参数传递到实现类初始化参数中,必须有 onGenerateRoute 拦截器配合才能实现。

▲ 如果只需要在 build 内通过 ModalRoute.of(context).settings.arguments 获取参数,则在这里不需要配置参数传递,像 ‘/’、’/page01’ 那样写即可。

再看一下onGenerateRoute拦截器的定义,粘贴示例代码如下:

Route<dynamic> onGenerateRoute(RouteSettings settings) {
  String routeName = settings.name;
  if (!routes.containsKey(routeName)) routeName = '/';
  final Function pageContentBuilder = routes[routeName];

  if (settings.arguments != null) {
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
      settings: settings,
    );
  } else {
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context),
      settings: settings,
    );
  }

示例拦截器中,先获取路由名称(因在MaterialApp中没有配置routes,所以每次创建命名路由页面都会触发这里)给routeName,然后查询是否存在于routes的key中,如果不在则把routeName的值更改为 ‘/’,后面就会加载根页。这是为了处理代码意外,避免名称错误查不到而造成应用停止。

然后使用 routeName 到 routes 获取映射的实现类给 pageContentBuilder。

settings.arguments 是通过 pushNamed 等命名路由方法传递的参数,在这里通过这个获取。

接下来判断 settings.arguments 是否为 null,如果不是 null 说明有传递参数,那在 MaterialPageRoute 加载路由页面时就要给实现类传参,否则就不需要。

settings: settings 这句是重中之重, 大部分文章中都没有这句,这句是为了让通过拦截器创建的新页面也有路由名称,主要使用的是 settings.name 属性。如果新创建的页面没有给名字,后续的使用页面名称操作路由时,就可能会出现问题。
.

示例不同情况路由栈内容分析

以打开应用后,以从home页一路点击按钮到page04为前提,在示例中页面page04点击不同的按钮,对路由栈内的情况有如下影响(以数组格式代替垂直表示的栈图,数组左侧为栈底):

  1. page04呈现后的路由栈:
    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’]
  2. 点击按钮 A 后的路由栈(使用pushNamed):
    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’]
  3. 点击按钮 B 后的路由栈(使用pushNamedAndRemoveUntil):
    [’/’, ‘/page01’, ‘/page02’]
  4. 点击按钮 C 后的路由栈(使用pushNamedAndRemoveUntil):
    [’/page02’]
  5. 点击按钮 D 后的路由栈(使用pushReplacementNamed):
    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’]
  6. 点击按钮 C 后的路由栈(使用popAndPushNamed):
    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’]
  7. 点击按钮 D 后的路由栈(使用popUntil):
    [’/’, ‘/page01’, ‘/page02’]
    .

我的感悟

虽说天下文章一大抄,但是技术问题还是需要真正的实践才能知晓真实的结果。很多技术源于国外,国内资料多为业余翻译版。当遇到问题很难搞定的时候,别忘记放下资料深入实践一下,可能得到的结论更直接。更多可以尝试自行翻译外文官网的相关片段,或许可以得到不一样信息。这里还是要感谢翻译外文技术资料的朋友们,辛苦了!

希望本文能给相关的后来者一定的解惑,若对本文内容有异议,请以官方文档为准。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学为所用

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值