Flutter之旅:路由导航

这章来聊聊flutter的路由管理,也可以理解为页面导航,用来处理页面之间的跳转、参数传递、动画展示等功能。

路由导航主要由跳转和返回两个操作,跳转是调用Navigator的push相关方法,返回是调用Navigator的pop相关方法,可以理解为push是将一个页面推送到路由栈中,pop是将一个页面从栈中移出。

push相关

先看一下Navigator中push的相关方法:


push.png

push

Navigator.push(context,Route);

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

该方法接受一个BuildContext和一个Route,context就不用说了,下面了解一下Route:


Route.png

简单一点看,Route分为了页面路由(PageRoute)和窗口路由(PopupRoute),而PopupRoute的默认实现均为私有,就是说如果以后要用到的话需要我们自己去实现。PageRoute默认提供了三个公开的实现类:

  • CupertinoPageRoute:Cupertino风格的默认实现。
  • MaterialPageRoute:Material风格的默认实现。
  • PageRouteBuilder:自定义PageRoute,比如一些动画效果。

示例代码:

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

另外push相关方法返回的都是一个Future,可以通过它来获取下一个页面的被pop时的返回值。

Navigator.push(context,MaterialPageRoute(builder: (context) => Page2()))
     .then((value) {
        print('page1 push $value');
});

完整代码

route_1.gif

pushReplacement

替换当前页面,并且当新页面动画执行完成之后,disposing前一个页面。

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

源码:

  @optionalTypeArgs
  static Future<T> pushReplacement<T extends Object, TO extends Object>(BuildContext context, Route<T> newRoute, { TO result }) {
    return Navigator.of(context).pushReplacement<T, TO>(newRoute, result: result);
  }

前两个参数同push,第三个可选参数result表示的是这个页面的返回结果,如果设置的话,会返回给被替换的这个页面的前一个页面

我们可以做这样一个操作:

  • 在Page1调用push方法跳转到Page2,并监听结果
  • 在Page2调用pushReplacement方法跳转Page3,并设置result


    route_2.gif

得到的日志如下:

I/flutter (14537): Page1 build
I/flutter (14537): Page1 push Page2
I/flutter (14537): Page2 build
I/flutter (14537): Page2 pushReplacement Page3 and result: Page2 result
I/flutter (14537): Page3 build
I/flutter (14537): Page1 push result: Page2 result

完整代码

pushAndRemoveUntil

跳转到指定页面,并按顺序(从栈顶到栈底)移出之前的所有页面,直到predicate返回true。

  @optionalTypeArgs
  static Future<T> pushAndRemoveUntil<T extends Object>(BuildContext context, Route<T> newRoute, RoutePredicate predicate) {
    return Navigator.of(context).pushAndRemoveUntil<T>(newRoute, predicate);
  }

typedef RoutePredicate = bool Function(Route<dynamic> route);

比如我从Page2调用跳转pushAndRemoveUntil到Page3,同时指定predicate的条件为route.settings.name == "/",那么跳转到Page3后Page2将被移除,因为第一个页面的默认RouteSetting的name属性值为"/"。

                Navigator.pushAndRemoveUntil(
                        context,
                        MaterialPageRoute(builder: (context) => Page3()),
                        (route) {
                          print('route:$route');
                          return route.settings.name == "/";
                        })
                    .then((value) {
                  print('Page2 pushAndRemoveUntil result: $value');
                });
route_3.gif

如果predicate的条件为route.settings.name != "/",那么任何一个页面都不会被移除,因为判断第一个前页面Page2的时候predicate已经返回true。


route_4.gif

pushNamed、pushReplacementNamed、pushNamedAndRemoveUntil

三者分别对应push、pushReplacement、pushAndRemoveUntil,提供了一种命名路由跳转,并且在flutter新版本中增加了一个可选参数arguments,用于页面之间的传参。路由的名字将会传递给Navigator的onGenerateRoute回调,并将返回的路由推入Navigator栈(具体可见下面的传参部分)。

这里以pushNamed方法为例,首先声明一个路由列表:

const String PAGE_2 = "/page2";

final Map<String, WidgetBuilder> _routes = {
  PAGE_2: (_) => Page2(),
};

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(routes: _routes, home: Page1());
  }
}

跳转的时候直接调用pushNamed传入路由对应的键值即可:

Navigator.pushNamed(context, PAGE_2);

对于pushNamedAndRemoveUntil的predicate参数,可以直接使用ModalRoute.withName(name)来指定。

pop相关

还是先看一下pop的相关方法:


pop.png

pop

从栈内移除最顶上的页面。

  @optionalTypeArgs
  static bool pop<T extends Object>(BuildContext context, [ T result ]) {
    return Navigator.of(context).pop<T>(result);
  }

可以接两个参数:

  • context:上下文。
  • result:即我们前面提到的返回给上一个页面的值。

popUntil

按顺序从栈内移除最顶上的页面,直到predicate返回true。predicate参数的含义可以参照上面的pushAndRemoveUntil。

  static void popUntil(BuildContext context, RoutePredicate predicate) {
    Navigator.of(context).popUntil(predicate);
  }

popAndPushNamed

就是pop和pushNamed两个方法的组合。

  @optionalTypeArgs
  Future<T> popAndPushNamed<T extends Object, TO extends Object>(
    String routeName, {
    TO result,
    Object arguments,
  }) {
    pop<TO>(result);
    return pushNamed<T>(routeName, arguments: arguments);
  }

页面传参

如果是非命名路由,即push系列方法,直接使用路由的构造函数传参即可:

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

如果是命名路由,之前是不可以传参的,新版本中增加了一个arguments参数,配合onGenerateRoute也可以传递参数,因为命名路由会将路由的名字传递给onGenerateRoute回调,并将产生的路由推入Navigator。

const String PAGE_2 = "/page2";
const String PAGE_3 = "/page3";

final Map<String, Function> _routes = {
  PAGE_2: (context, {arguments}) => Page2(arguments: arguments),
  PAGE_3: (context, {arguments}) => Page3(arguments: arguments),
};

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Page1(),
      onGenerateRoute: (routeSetting) {
        Function _routeGenerate = _routes[routeSetting.name];
        if (_routeGenerate != null)
          return MaterialPageRoute(
              builder: (context) => _routeGenerate(context, arguments: routeSetting.arguments));
      },
    );
  }
}
class Page2 extends StatelessWidget {
  Map<String, Object> arguments;
  Page2({this.arguments});
    @override
  Widget build(BuildContext context) {
    print('Page2 build');
    print('arguments:$arguments');
    ...
  }
}
Navigator.pushNamed(context, PAGE_2,arguments: {"name":"lili"});

得到日志如下:

I/flutter (24397): Page1 pushNamed Page2
I/flutter (24397): Page2 build
I/flutter (24397): arguments:{name: lili}

切换动画

如果想自定义页面的切换效果,我们可以使用PageRouteBuilder来自定义路由。

  PageRouteBuilder({
    RouteSettings settings,
    @required this.pageBuilder,
    this.transitionsBuilder = _defaultTransitionsBuilder,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.opaque = true,
    this.barrierDismissible = false,
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
  }) : assert(pageBuilder != null),
       assert(transitionsBuilder != null),
       assert(barrierDismissible != null),
       assert(maintainState != null),
       assert(opaque != null),
       super(settings: settings);

settings

路由相关设置,名字、参数、是否初始路由,如果为空,则会生成一个默认的。

Route({ RouteSettings settings }) : settings = settings ?? const RouteSettings();

pageBuilder

用来构建路由的主要内容。可以查看ModalRoute.buildPage方法来了解它的参数信息。

typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
  • context:正在构建的路由的上下文。
  • animation:路由的主变换动画,如果是进入,值从0.0逐渐变化到1.0;如果是退出,值从1.0逐渐变化到0.0。
  • secondaryAnimation:路由的次变换动画。

transitionsBuilder

用于构建路由的变换效果。可以通过ModalRoute.buildTransitions方法来了解它的参数信息。

typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
  • context:正在构建的路由的上下文。
  • animation:路由的主变换动画,如果是进入,值从0.0逐渐变化到1.0;如果是退出,值从1.0逐渐变化到0.0。
  • secondaryAnimation:路由的次变换动画。当把一个新的路由push到栈顶时,原栈顶的路由的secondaryAnimation值从0.0变化到1.0;当栈顶路由被pop的时候,它下面的那个路由的secondaryAnimation值从1.0变化到0.0。
  • child:页面的内容,即pageBuilder返回的widget。

transitionDuration

变换效果的持续时间。

opaque

是否不透明,默认为true,如果是不透明的话,路由变换完成之后,不会再构建位于该路由之下的路由,以节省资源。

barrierColor

模态屏障的颜色。如果为null,则屏障将是透明的。比如弹出一个对话框时,背景可以设置成灰暗的。注意Dialog也是一个路由。

Future<T> showGeneralDialog<T>({
  @required BuildContext context,
  @required RoutePageBuilder pageBuilder,
  bool barrierDismissible,
  String barrierLabel,
  Color barrierColor,
  Duration transitionDuration,
  RouteTransitionsBuilder transitionBuilder,
}) {
  assert(pageBuilder != null);
  assert(!barrierDismissible || barrierLabel != null);
  return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
  ));
}

可以看到其实Dialog就是一个_DialogRoute。

barrierDismissible

点击屏障是否自动消失。
我们来弹出一个Dialog验证一下,设置屏障颜色为半透明红色,点击屏障自动消失:

                showGeneralDialog(
                    context: context,
                    barrierDismissible: true,
                    barrierLabel: "Dismiss",
                    barrierColor: Color.fromRGBO(255, 0, 0, 0.5),
                    transitionDuration: Duration(milliseconds: 300),
                    pageBuilder: (context, animation, secondaryAnimation) {
                      return AlertDialog(
                        title: Text("标题"),
                      );
                    });
route_5.gif

maintainState

当路由为inactive状态时,是否需要在内存中保存路由状态。

示例

我们来做一个简单的旋转渐隐的动画效果。

                Navigator.push(
                    context,
                    PageRouteBuilder(
                        pageBuilder: (context, animation, secondaryAnimation) {
                          return Page2();
                        },
                        transitionsBuilder:
                            (context, animation, secondaryAnimation, child) {
                          return FadeTransition(
                            opacity: animation,
                            child: RotationTransition(
                              turns: Tween(begin: 0.0, end: 1.0)
                                  .animate(animation),
                              child: child,
                            ),
                          );
                        },
                        transitionDuration: Duration(milliseconds: 500)));
route_6.gif

共享元素动画

做过android的对这个一定不陌生,这里提一下在flutter中的简单实现。

使用Hero包裹要共享的widget,并设置相同的tag。

            Hero(
                tag: "btnBack",
                child: RaisedButton(
                  onPressed: () {
                    print('Page2 pop');
                    Navigator.pop(context);
                  },
                  child: Text("返回"),
                )),

...

            Hero(
                tag: "btnBack",
                child: RaisedButton(
                  onPressed: () {
                    print('Page3 pop');
                    Navigator.pop(context);
                  },
                  child: Text("返回"),
                )),
route_7.gif

完整代码

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