码个蛋(codeegg)第 590 次推文

上一节撸了个界面,虽然比较简单,但是把前面讲的知识串联了下,但是界面之间的跳转一直没说,这节就讲下 Flutter 中的「路由」来管理界面。

Navigator

Flutter 通过 Navigator  来进行页面之间的跳转,分为 push 系列和 pop 系列操作,带 push 方法为入栈操作,带 pop 方法为出栈操作。Navigator 的 push 方法分两类,一类是带 Name 的,需要在 MaterialApp 下将 routers 属性进行注册,否则将会找不到该路由,还有一个是不带 Name 的,可以通过 Router 直接跳转。

说那么多相信还不如直接上代码和图来的更直接。因为需要展示所有的跳转至少需要 3 个页面,所以我们创建最简单的三个界面,通过文字来区别不同的页面,因为需要调用带有 Name 的方法,所以需要先在 MaterialApp 对路由进行注册。

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Learning Demo',
      // 在这里注册路由,关联 name 和界面
      // '/' 表示根页面,也就是 home 所对应的页面,这边就不需要配置 home 属性了
      routes: {'/': (_) => APage(), '/page_b': (_) => BPage(), '/page_c': (_) => CPage()},
      debugShowCheckedModeBanner: false,
    );
  }
}


/// Page A,Button 的跳转事件等会进行修改,目前先空着
class APage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Page A'),
      ),
      body: Center(child: RaisedButton(onPressed: () {}, child: Text('To Page B'))),
    );
  }
}


/// Page B
class BPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Page B'),
      ),
      body: Center(
          child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
        RaisedButton(onPressed: () {}, child: Text('To Page C')),
        RaisedButton(onPressed: () {}, child: Text('Back Page A'))
      ])),
    );
  }
}


/// Page C
class CPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Page C'),
      ),
      body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[RaisedButton(onPressed: () {}, child: Text('Back Last Page'))])),
    );
  }
}

push / pushNamed 方式跳转

我们在 APage 的 RaiseButton 的 onPressed 方法加入如下代码

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

或者

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

效果相同。跳转后,可以发现,在 BPage 的 AppBar 上有个返回按钮,点击可以返回 APage ,那么也就是说通过 push 或者 pushNamed 方式跳转的时候,界面堆栈的变化是直接在原来的堆栈上添加一个新的 page

为了凸显堆栈的变化,所以绘制的图中,会比使用的实际页面多一个,下图同

iOS 打开 flutter 指定路由页面 flutter web 路由_sed

pushReplacement / pushReplacementNamed / popAndPushNamed

将 APage 中的跳转方式进行替换

Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => BPage()));

或者

Navigator.pushReplacementNamed(context, '/page_b');

或者

/ 如果是第一个界面跳转到下个界面,勿用,`BPage` 会显示返回按钮,但是点击后,界面会变黑
// 因为 `APage` 已经不在堆栈中了,点击后堆栈就没有 `Page` 了,所以界面变黑
Navigator.popAndPushNamed(context, '/page_b');

效果相同,跳转后,可以发现 BPage 的返回按钮消失了,消失了,消失了,我们可以试下点击返回按键,发现 App 直接退出了,也就是说,BPage 替代了 APage 在堆栈中的位置。那么堆栈的变化图就是这样的

iOS 打开 flutter 指定路由页面 flutter web 路由_返回按钮_02

pushAndRemoveUntil / pushNamedAndRemoveUntil

CASE 1

这个跳转方式需要通过 CPage 来协助完成,将 APage 的跳转方式修改为 push 方式,然后在 BPage 的第一个按钮加入如下代码

Navigator.pushAndRemoveUntil(context, 
                   MaterialPageRoute(builder: (_) => CPage()), (Route router) => false);

或者

Navigator.pushNamedAndRemoveUntil(context, '/page_c', (Route router) => false);

效果相同,点击 BPage 的跳转 CPage 按钮后,界面来到 CPage,然后发现还是没有返回按钮,没有返回按钮,没有返回按钮,点击下返回按键,然后发现 App 直接退出了,退出了,退出了,那么堆栈变化如图

iOS 打开 flutter 指定路由页面 flutter web 路由_堆栈_03

CASE 2

你以为这两个方法只是为了把堆栈都清空吗,那就太图样图森破了,这边展示另一种。修改跳转的代码

Navigator.pushAndRemoveUntil(context, 
                MaterialPageRoute(builder: (_) => CPage()), ModalRoute.withName('/'));

或者

Navigator.pushNamedAndRemoveUntil(context, '/page_c', ModalRoute.withName('/'));

点击跳转 CPage 以后,发现返回按钮又回来了...就这么回来了...只是修改了一个参数,点击返回按钮,又回到了 APage,你可以在 APage 跳转 BPage 中加入DPage EPage 等等更多的界面,只要保证 BPage 跳转 CPage 的方式不变,点击 CPage 的返回按钮,又回到 APage 了,所以...堆栈的变化图如下

iOS 打开 flutter 指定路由页面 flutter web 路由_堆栈_04

SUMMARY

为什么会这样变化呢,还记得在 MaterialApp 中注册的 router 么,APage 的 name 对应的为 '/',也就是说,该方法会把堆栈中在 ModalRoute.withName 所对应的 page 上的所有都 pop 出堆栈,如果把参数换成 /page_b,然后在跳转 CPage 之前加入更多的界面,点击 CPage 的返回按钮,就会回到 BPage

pop

在 BPage 的第二个按钮中加入 pop 操作

Navigator.pop(context);

跳转到 BPage 后点击该按钮,界面回到 APage,那么堆栈的变化很明显了,如图

iOS 打开 flutter 指定路由页面 flutter web 路由_堆栈_05

popUntil

这个方法还需要借助 CPage ,在 CPage 的按钮中加入

Navigator.popUntil(context, ModalRoute.withName('/'));

点击返回按钮,界面跳过 BPage 回到了 APage,解释同 pushAndRemoveUntil 那么堆栈的变化也显而易见咯

iOS 打开 flutter 指定路由页面 flutter web 路由_返回按钮_06

Navigator 传值

CASE 1 传值给下个界面

修改下 BPage 和 APage 的按钮点击事件

class BPage extends StatelessWidget {
  final String message;


  BPage({Key key, @required this.message}) : super(key: key);


  @override
  Widget build(BuildContext context) {
    print('passed value: $message');
    return Scaffold(
      // 省略相同代码
    );
  }
}


// APage 跳转事件
Navigator.push(context, MaterialPageRoute(builder: 
                                          (_) => BPage(message: 'Message From Page A')));

点击 APage 可以查看控制台有输出

2019-03-17 00:04:06.854 12868-12888/com.kuky.demo.flutterartsdemosapp I/flutter: passed value: Message From Page A

也就是成功把值传递过来了。但是,需要传递参数的话,之前在 MaterialApp 下注册的路由就需要去除了。

CASE 2 传值给上个界面

这边可以查看下 pop 方法

@optionalTypeArgs
  // pop 可以传入一个可选参数 result,这个 result 也就是回传给上个页面的参数值了
  static bool pop<T extends Object>(BuildContext context, [ T result ]) {
    return Navigator.of(context).pop<T>(result);
  }

既然知道 pop 如何传递值给上个界面,那么如何在上个界面接收这个参数呢,还是看下 push 方法

@optionalTypeArgs
  static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
  }


///
@optionalTypeArgs
  Future<T> push<T extends Object>(Route<T> route) {
    // ...省略无关代码
    // 这边返回一个 Future 值,`pop` 所传递的值会在这边返回
    return route.popped;
  }


/// The future completes with the value given to [Navigator.pop], if any.
Future<T> get popped => _popCompleter.future;

官方的注释非常明白的指出,会在 Future 中携带 pop 传递的参数,那么我们对 APage跳转 BPage 以及 BPage 返回 APage 的逻辑进行修改

/// APage
Navigator.push(context, MaterialPageRoute(builder: (_) 
                                          => BPage(message: 'Message From Page A')))
                    .then((value) => print('BACK MESSAGE => $value'));
                    
                    /// BPage
Navigator.pop(context, 'Message back to PageA From BPage');

点击返回后,能够在控制台发现有如下输入

2019-03-17 16:35:53.820 13417-13442/com.kuky.demo.flutterartsdemosapp I/flutter: BACK MESSAGE => Message back to PageA From BPage

上个页面成功接收到下个页面回传的数据。

CASE 3 通过系统返回按钮传值

在 CASE 2 情况下,通过按钮对返回事件进行监听,那加入我们需求没有这个按钮,只能通过系统默认的返回按钮,或者物理返回按键,那该如何传值呢,这里就需要用 WillpopScope 对系统的返回按钮进行监听。我们对 CPage 做下修改,在 Scaffold 外面包裹一个 WillpopScope

class CPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        child: Scaffold(
          appBar: AppBar(
            title: Text('Page C'),
          ),
          body: Center(
              child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
            RaisedButton(
                onPressed: () {
                  Navigator.popUntil(context, ModalRoute.withName('/'));
                },
                child: Text('Back Last Page'))
          ])),
        ),
        // 这里对系统返回按钮做监听..
        // 如果返回的是 `true` 则相当于 `pop` 操作,返回 `false` 则只执行上一步的 `pop` 操作
        // 例如双击返回退出,也是通过 `WillpopScope` 来进行监听
        onWillPop: () async {
          Navigator.pop(context, 'Hello~');
          return false;
        });
  }
}

通过返回按钮,BPage 会成功收到从 CPage 返回的 Hello~ 值

以上代码查看 router_main.dart 文件

路由切换动画

假如说我们不想用系统自带的切换动画,需要弄一些比较酷炫的效果该怎么办,那就需要用到自定义路由切换动画了。直接修改 BPage 跳转 CPage 的代码

Navigator.push(
    context,
    PageRouteBuilder(
        // 返回目标页面
        pageBuilder: (context, anim, _) => CPage(),
        // 切换动画的切换时长
        transitionDuration: Duration(milliseconds: 500),
        // 切换动画的切换效果,系统自带的常用 Transition
        // ScaleTransition: 缩放  SlideTransition: 滑动
        // RotationTransition: 旋转  FadeTransition: 透明度
        transitionsBuilder: (context, anim, _, child) => ScaleTransition(
              // Tween 是 flutter 的补间动画,等讲到动画的时候再提吧,这边先记住这么使用
              scale: Tween(begin: 0.0, end: 1.0).animate(anim),
              // 这个值必须记得要传,否则会不显示界面
              child: child,
            )));

当再次点击跳转的时候,切换的动画就有开始自带的平滑效果变成缩放效果了。那如果要实现多个动画呢,例如边缩放,边改变透明度,也很容易实现,只需要将 child 替换成 Transition 即可

Navigator.push(
    context,
    PageRouteBuilder(
        pageBuilder: (context, anim, _) => CPage(),
        transitionDuration: Duration(milliseconds: 500),
        transitionsBuilder: (context, anim, _, child) => ScaleTransition(
              scale: Tween(begin: 0.0, end: 1.0).animate(anim),
              // 替换即可,如果要加入更多的动画,替换 `child` 属性就可以了
              child: FadeTransition(
                opacity: Tween(begin: 0.0, end: 1.0).animate(anim),
                child: child,
              ),
            )));

当然,为了方便重复利用,需要进行封装,例如我们要封装上面的缩放动画效果

class ScalePageRoute extends PageRouteBuilder {
  final Widget widget;


  ScalePageRoute(this.widget)
      : super(
            transitionDuration: Duration(milliseconds: 500),
            pageBuilder: (context, anim, _) => widget,
            transitionsBuilder: (context, anim, _, child) => ScaleTransition(
                  scale: Tween(begin: 0.0, end: 1.0).animate(anim),
                  child: child,
                ));
}

然后直接在 Navigator 跳转的时候调用该 Route 就可以了

该部分代码查看 custom_routes.dart 文件

还记得我们之前写的 demo 都是单个文件写一个入口的吗,现在我们就可以写一个统一管理的页面,对这些界面进行管理了,这个工作就交给大家伙自己了,当然我也在源码做了修改,可以查看 main.dart 文件

代码地址:

https://github.com/kukyxs/flutter_arts_demos_app