路由与导航,Flutter是这样实现页面切换的

210 阅读5分钟

如果说 UI 框架的视图元素的基本单位是组件,那应用程序的基本单位就是页面了。对于拥有多个页面的应用程序而言,如何从一个页面平滑地过渡到另一个页面,我们需要有一个统一的机制来管理页面之间的跳转,通常被称为路由管理或导航管理。

路由管理

在 Flutter 中,页面之间的跳转是通过 Route 和 Navigator 来管理的:

Route 是页面的抽象,主要负责创建对应的界面,接收参数,响应 Navigator 打开和关闭;

而 Navigator 则会维护一个路由栈管理 Route,Route 打开即入栈,Route 关闭即出栈,还可以直接替换栈内的某一个 Route。

而根据是否需要提前注册页面标识符,Flutter 中的路由管理可以分为两种方式:

基本路由。无需提前注册,在页面切换时需要自己构造页面实例。

命名路由。需要提前注册页面标识符,在页面切换时通过标识符直接打开新的路由。

基本路由

在 Flutter 中,基本路由的使用方法和 Android/iOS 打开新页面的方式非常相似。要导航到一个新的页面,我们需要创建一个 MaterialPageRoute 的实例,调用 Navigator.push 方法将新页面压到堆栈的顶部。

其中,MaterialPageRoute 是一种路由模板,定义了路由创建及切换过渡动画的相关配置,可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。

而如果我们想返回上一个页面,则需要调用 Navigator.pop 方法从堆栈中删除这个页面。

下面的代码演示了基本路由的使用方法:在第一个页面的按钮事件中打开第二个页面,并在第二个页面的按钮事件中回退到第一个页面:

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      //打开页面
      onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // 回退页面
      onPressed: ()=> Navigator.pop(context)
    );
  }
}

命名路由

基本路由使用方式相对简单灵活,适用于应用中页面不多的场景。而在应用中页面比较多的情况下,再使用基本路由方式,那么每次跳转到一个新的页面,我们都要手动创建 MaterialPageRoute 实例,初始化页面,然后调用 push 方法打开它,还是比较麻烦的。

要想通过名字来指定页面切换,我们必须先给应用程序 MaterialApp 提供一个页面名称映射规则,即路由表 routes,这样 Flutter 才知道名字与页面 Widget 的对应关系。

路由表实际上是一个 Map<String,WidgetBuilder>,其中 key 值对应页面名字,而 value 值则是一个 WidgetBuilder 回调函数,我们需要在这个函数中创建对应的页面。而一旦在路由表中定义好了页面名字,我们就可以使用 Navigator.pushNamed 来打开页面了。

下面的代码演示了命名路由的使用方法:在 MaterialApp 完成了页面的名字 second_page 及页面的初始化方法注册绑定,后续我们就可以在代码中以 second_page 这个名字打开页面了:

MaterialApp(
    ...
    //注册路由
    routes:{
      "second_page":(context)=>SecondPage(),
    },
);
//使用名字打开页面
Navigator.pushNamed(context,"second_page");

不过由于路由的注册和使用都采用字符串来标识,这就会带来一个隐患:如果我们打开了一个不存在的路由会怎么办?

在注册路由表时,Flutter 提供了 UnknownRoute 属性,我们可以对未知的路由标识符进行统一的页面跳转处理。

下面的代码演示了如何注册错误路由处理。和基本路由的使用方法类似,我们只需要返回一个固定的页面即可。

MaterialApp(
    ...
    //注册路由
    routes:{
      "second_page":(context)=>SecondPage(),
    },
    //错误路由处理,统一返回UnknownPage
    onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);

//使用错误名字打开页面
Navigator.pushNamed(context,"unknown_page");

页面参数

与基本路由能够精确地控制目标页面初始化方式不同,命名路由只能通过字符串名字来初始化固定目标页面。为了解决不同场景下目标页面的初始化需求,Flutter 提供了路由参数的机制,可以在打开路由时传递相关参数,在目标页面通过 RouteSettings 来获取页面参数。

下面的代码演示了如何传递并获取参数:使用页面名称 second_page 打开页面时,传递了一个字符串参数,随后在 SecondPage 中,我们取出了这个参数,并将它展示在了文本中。

//打开页面时传递字符串参数
Navigator.of(context).pushNamed("second_page", arguments: "Hey");

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出路由参数
    String msg = ModalRoute.of(context).settings.arguments as String;
    return Text(msg);
  }
}

除了页面打开时需要传递参数,对于特定的页面,在其关闭时,也需要传递参数告知页面处理结果。

与 Android 提供的 startActivityForResult 方法可以监听目标页面的处理结果类似,Flutter 也提供了返回参数的机制。在 push 目标页面时,可以设置目标页面关闭时监听函数,以获取返回参数;而目标页面可以在关闭路由时传递相关参数。

下面的代码演示了如何获取参数:在 SecondPage 页面关闭时,传递了一个字符串参数,随后在上一页监听函数中,我们取出了这个参数,并将它展示了出来。

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Text('Message from first screen: $msg'),
          RaisedButton(
            child: Text('back'),
            //页面关闭时传递参数
            onPressed: ()=> Navigator.pop(context,"Hi")
          )
        ]
      ));
  }
}

class _FirstPageState extends State<FirstPage> {
  String _msg='';
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: Column(children: <Widget>[
        RaisedButton(
            child: Text('命名路由(参数&回调)'),
            //打开页面,并监听页面关闭时传递的参数
            onPressed: ()=> Navigator.pushNamed(context, "second_page",arguments: "Hey").then((msg)=>setState(()=>_msg=msg)),
        ),
        Text('Message from Second screen: $_msg'),

      ],),
    );
  }
}

对于基本路由,如何传递页面参数?

  1. 使用构造函数传参
  2. MaterialPageRoute 加入参数 setting: RouteSettings 。第二个页面获取逻辑和命名路由就一样了。
Navigator.push(
                context,
                MaterialPageRoute(
                  // 传递title为SecondPage,跳转到第二个界面就会把标题设置为SecondPage
                  builder: (context) => SecondPage(title: "SecondPage"),
                ));
          },

多参数传参

传参:
RaisedButton(
            child: Text('命名路由(参数&回调)'),
            onPressed: () =>
                Navigator.pushNamed(context, "third_page", arguments: [1, 2])
                    .then((msg) {
              setState(() {
                _msg = msg;
              });
            })

计算并返回:

class ThirdPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var args = ModalRoute.of(context).settings.arguments as List<int>;

    int sum = args[0] + args[1];
    print(sum);

    return Scaffold(
      appBar: AppBar(
        title: Text('Third Screen'),
      ),
      body: Column(
        children: <Widget>[
          Text('Message from first screen:  $args'),
          RaisedButton(
            child: Text('back'),
            onPressed: ()=> Navigator.pop(context, "${sum}"),
          )
        ],
      ),
    );
  }
}