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

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

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

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

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

Navigator_pushAndRemoveUnit2.png
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,那么堆栈的变化很明显了,如图

Navigator_pop.png
popUntil

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

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

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

Navigator_popUntil.png

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,先谢过,你的认可就是支持我继续写下去的动力~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容