Flutter 入门指北(Part 6) 之路由

该文已授权公众号 「码个蛋」,转载请指明出处

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

Navigator

Flutter 通过 Navigator 来进行页面之间的跳转,分为 push 系列和 pop 系列操作,带 push 方法为入栈操作,带 pop 方法为出栈操作。Navigatorpush 方法分两类,一类是带 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 方式跳转

我们在 APageRaiseButtononPressed 方法加入如下代码

Navigator.push(context, MaterialPageRoute(builder: (_) => BPage()));
复制代码

或者

Navigator.pushNamed(context, '/page_b');
复制代码

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

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

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 在堆栈中的位置。那么堆栈的变化图就是这样的

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 直接退出了,退出了,退出了,那么堆栈变化如图

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 了,所以...堆栈的变化图如下

######SUMMARY

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

QUESTION

这边再提个小问题,有页面 A,B,C,D,其路由的 name 分别为 '/','page_b','page_c','page_d',启动顺序为 A -> B -> C -> C -> D,那么在 D 页面使用

Navigator.pushNamedAndRemoveUntil(context, '/page_c', ModalRoute.withName('/page_c'));
复制代码

那么堆栈最后剩下的页面是 ABCC 还是ABC 呢?答案会在最后公布,小伙伴可以先自己尝试着实现。

pop

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

Navigator.pop(context);
复制代码

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

popUntil

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

Navigator.popUntil(context, ModalRoute.withName('/'));
复制代码

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

Navigator 传值

######CASE 1 传值给下个界面

修改下 BPageAPage 的按钮点击事件

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 文件

在前面有提出一个问题,这边公布下答案:堆栈中的页面应该为 ABCC。你答对没有呢~

最后代码的地址还是要的:

  1. 文章中涉及的代码:demos

  2. 基于郭神 cool weather 接口的一个项目,实现 BLoC 模式,实现状态管理:flutter_weather

  3. 一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,但是还是做了自己的修改,很多地方看着不舒服,然后就改成自己的实现方式了):flutter_shop

如果对你有帮助的话,记得给个 Star,先谢过,你的认可就是支持我继续写下去的动力~

转载于:https://juejin.im/post/5cb33ff6f265da03a1581d35

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值