路由导航与跨页传参

1,889 阅读6分钟

欢迎点赞,转载请注明出处

本节示例项目源代码下载点击这里 flutter_route

一个简单的路由

在Flutter中,屏(screen)和页面(page)都叫做路由(route)。在Android开发中,Activity相当于“路由”,在iOS开发中,ViewController 相当于“路由”。在Flutter中,“路由”也是一个Widget。使用Navigator类,可以实现Flutter的路由之间的跳转。
使用Navigator.push()方法跳转到新的路由。push()方法会添加一个Route对象到导航器的堆栈上。可以直接使用MaterialPageRoute或CupertinoPageRoute类。

 Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SomeWidget()),
  );

其中build参数为WidgetBuilder函数自定义类型:Widget Function(BuildContext context)。
使用Navigator.pop() 方法会从导航器堆栈上移除 Route对象。

一个简单的官方路由跳转示例如下:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute()),
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

构造函数传参

大多数时候,路由之间跳转需要传递参数。最简单的方式,我们只需要在目的路由的构造函数增加入参接收要传递的数据即可,在上面例子的基础上,我们做了一点修改,修改的部分见下图代码红框部分:

RouteSetting传参

我们还可以使用RouteSetting的方式,在目的路由使用ModalRoute.of(context).settings.arguments获取传递的参数。路由跳转时,在MaterialPageRoute增加RouteSettings入参类型,将要传递的入参赋值给它的arguments参数。在第一个例子的基础上,我们做了一点修改,修改的部分见下图代码红框部分:

命名路由

如果在一个App的不同页面需要导航到同一路由时,每次都需要调用重复的Navigator.push代码段。我们可以定义一种命名路由来简化这一操作。
命名路由需要在MaterialApp或Cupertino构造函数中定initialRoute和routes参数。 initialRoute定义了App第一个进入的页面。 routes属性定了可用的命名路由集合,使用Navigator.pushNamed()调用的方式,代替Navigator.push()方式,pushNamed方法第2个参数对应。第一个例子改成命名路由的方式代码如下:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Named Route',
    initialRoute: '/',
    routes: {
      '/': (context) => FirstRoute(),
      '/second': (context) => SecondRoute(),
    },
  ));
}

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/second',
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

原先示例中的,MaterialApp构造函数的home参数,对应的"/"路由,我们可以把上例的MaterialApp构造函数改为如下方式,运行效果与修改前是一样的。

MaterialApp(
    title: 'Named Route',
    home: FirstRoute(),
    routes: {
          '/second': (context) => SecondRoute(),
    },
  )

initialRoute值一般为'/',当然也可以指定一个具体的路由名称。routes参数里则必须定义 '/',因为initialRoute路由名无效时,应用会自动匹配'/'指向的路由。示例代码如下:

initialRoute: '/first',
    routes: {
      '/': (context) => FirstRoute(),
      '/first': (context) => FirstRoute(),
      '/second': (context) => SecondRoute(),
    },

路由返回到首页同样使用的是Navigator.pop(context),也可以使用下面的代码返回到首页。如果你了解Stack堆栈数据结构的概念的话,这两者之间的差异是显然易见的:

Navigator.pushNamed(
              context,
              '/first',
            );

Navigator类并没有公开查询堆栈数组的属性或方法,如果你有兴趣的话,可以通过调试模式下,观察Navigator.of(context)的_history变量:

命名路由传参

你同样可以使用构造函数和RouteSetting的类似方式为命令路由传参。pushName可以直接使用arguments命名参数传参,而不需要RouteSetting进行参数包裹,代码类似如下:

Navigator.pushNamed(
              context,
              '/second',
              arguments: times++
            );

对于命名路由还有另外一种处理的方式,在MaterialApp或CupertinoApp构造函数里使用onGenerateRoute()函数来构建目的路由,同时对路由的参数进行赋值处理,代码如下:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Named Route',
    initialRoute: '/',
    routes: {
      '/': (context) => FirstRoute()
    },
    onGenerateRoute: (settings) {
      if (settings.name == '/second') {
        final  _times = settings.arguments;
        return MaterialPageRoute(
          builder: (context) {
            return SecondRoute(
                _times
            );
          },
        );
      }else{
        throw "mismatch route";
      }
    },
  ));
}

class FirstRoute extends StatelessWidget {
  var times = 1;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/second',
              arguments: times++
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {

  var pushed;
  SecondRoute(this.pushed);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("#$pushed opend"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

观察上面的代码可以看到,当routes里没有'/second'路由定义时,则系统会去onGenerateRoute函数里进行匹配处理 if (settings.name == '/second'),如果onGenerateRoute函数里也匹配不到,则抛出异常。

路由数据返回

我们有时候希望路由跳转返回后,能够从上一个路由返回一些数据进行逻辑处理,在Android里可以使用startActivityForResult()方法,Flutter写法如下:

mport 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Route return data',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatefulWidget {
  @override
  FirstState createState() => FirstState();
}

class FirstState extends State<FirstRoute> {
  var dateInfo;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text(dateInfo??="请问现在几点了?"),
          onPressed: ()  async {
            var _return = await Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute()),
            );
            setState(() {
              dateInfo = _return+"点";
            });
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            Navigator.pop(context, DateTime.now().hour.toString());
          },
          child: Text('点我'),
        ),
      ),
    );
  }
}

为了功能演示需要,我们把FirstRoute改为StatefulWidget类型,在onPressed事件里,使用await Navigator.push的方式进行路由跳转。查看Navigator.push函数官方api说明,可以看到push的返回值实际上Future<T>类型:

@optionalTypeArgs
Future<T> push <T extends Object>(
BuildContext context,
Route<T> route
)

上例的T代表就是String类型。关键字await表示同步等待一个过程的完成,此处就是等待Navigator的pop事件的完成,pop事件的第2个参数作为Future的完成值返回。await必须在async异步函数中执行,因此,我们在onPressed: ()后增加了async修饰符。上述示例的代码运行效果如下图,其中左图代表App运行的初始状态,中图代表导航到第2个路由后的状态,右图代表从第二个路由返回到第一个路由的效果,可以看到DateTime.now().hour.toString()返回给_return变量,并通过setState()更新result达到同步更新界面的效果:

路由跳转动画

MaterialPageRoute或CupertinoPageRoute类控制的路由之间跳转风格是固定的。我们可以使用push函数的第2个参数PageRouteBuilder自定义路由跳转的动画效果,你需要了解一些动画基本原理和Flutter动画相关语法,但这并不在本教程讨论范围之内。你有兴趣的话,可以运行下面代码,观察下它的跳转动画效果,并展开相关的深入学习。

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Navigation Animation',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.push(
              context,
              PageRouteBuilder(
                pageBuilder: (c, a1, a2) => SecondRoute(),
                transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
                transitionDuration: Duration(milliseconds: 2000),
              ),
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

实验九

在实验八的基础上,考虑增加以下功能:

  1. 登录页面增加一个同意协议功能,点击登录页阅读协议按钮导航到新页面,展示一个协议文本Demo,底部设置‘接受’和'拒绝'2个按钮;
  2. 接受协议,且账号和密码正确,点击登录按钮导航到新的页面,新页面上显示账号名;
  3. 在新的页面增加一个注销按钮,点击后,返回登录首页。

上一篇 UI交互控制 下一篇 Widget状态和应用数据管理