阅读 799

Flutter系列笔记-7:Flutter路由管理

没有注释的代码不是好代码

没有demo的博客不是好博客

本博客代码请移步github

什么是路由管理

Flutter里的路由管理( NavigatorRoutes)对应安卓里的Intent

Android 中,Intent 主要有两个使用场景:在 Activity 之前进行导航,以及组件间通信

Flutter 实际上并没有 ActivityFragment 的对应概念。在 Flutter 中你需要使用 NavigatorRoute 在同一个 Activity 内的不同界面间进行跳转。

Route 是应用内屏幕和页面的抽象,Navigator 是管理路径 route 的工具。

一个 route 对象大致对应于一个 Activity,但是它的含义是不一样的。Navigator 可以通过对 route 进行压栈和弹栈操作实现页面的跳转。

Navigator 的工作原理和栈相似,你可以将想要跳转到的 route 压栈 (push()),想要返回的时候将 route 弹栈 (pop())。 可以查看相应的翻译了解 更多

关于IOS里相对应的概念可以查看这里

Navigator相关路由管理方法

of 主要是获取NavigatorState对象对路由进行push pop replace remove等操作

普通路由管理方法

push 将设置的router信息推送到Navigator上,实现页面跳转。

pop 关闭当前页面

popUntil 反复执行pop 直到该函数的参数predicate返回true为止。

pushAndRemoveUntil 将给定路由推送到Navigator,一个一个地删除先前的路由,直到该函数的参数predicate返回true为才停止。

pushReplacement 用新的路由替换当路由。

命名路由管理方法

pushNamed 效果等同于push

pushNamedAndRemoveUntil 效果等同pushAndRemoveUntil

pushReplacementNamed 效果同pushReplacement

popAndPushNamed 关闭当前页面,并导航到新页面

其他方法

canPop 判断是否可以导航到新页面

maybePop 可能会导航到新页面

removeRoute 从Navigator中删除指定的路由,同时执行Route.dispose操作。

removeRouteBelow 从Navigator中删除指定路由的前一个路由,同时执行Route.dispose操作

replace 将Navigator中指定的路由替换成一个新路由。

replaceRouteBelow 将Navigator中的指定路由的前一个路由,替换成相应的路由

1.Navigator.push 跳转到新页面

先看Navigator.push方法的定义 接收两个参数,并且有返回值Future,可以异步获取到返回值

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

Route可以直接使用的子类有

  1. MaterialPageRoute 安卓风格 从下往上淡入 退出时相反

     Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
       return PublishBlogPage();
     }));
    复制代码
  2. CupertinoPageRoute ios风格 进入时动画从右侧往左滑入,退出时相反

     Navigator.push(context, CupertinoPageRoute(builder: (BuildContext context) {
       return PublishBlogPage();
     }));
    复制代码
  3. PageRouteBuilder 这个有一个必须外部传入的pageBuilder参数,一般用于自定义页面跳转动画

     Navigator.push(context, PageRouteBuilder(pageBuilder: (BuildContext context,
         Animation<double> animation, Animation<double> secondaryAnimation) {
       return new FadeTransition(
         //使用渐隐渐入过渡,
         opacity: animation,
         child: PublishBlogPage(),
       );
     }));
    复制代码

获取第二个页面的返回结果

可以使用then

Navigator.push(context, CupertinoPageRoute(builder: (BuildContext context) {
  return PublishBlogPage();
})).then((result){
  print('result:$result');
});
复制代码

也可以使用await ,推荐使用 async/await

void _publishBlog(BuildContext context) async {
  //MaterialPageRoute 安卓风格 从下往上淡入 退出时相反
  var reslut = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
    return PublishBlogPage();
  }));
  print('result:$reslut');
}
复制代码

  如果使用Navigator.push方法跳转到的第二个界面使用了Scaffold作为根Widget并设置了appBar,appBar会默认有一个返回按钮,点击返回按钮或者模拟器的返回键,可以关闭页面,返回时返回给上一个页面的结果是null。,如果自定义的页面不使用Scaffold,没有返回按钮怎么办,或者需要返回自定义的结果给上一个页面时怎么办,可以使用 Navigator.pop(context) 或者Navigator.of(context).pop()

2.Navigator.pop 关闭当前页面

关闭当前页面可以使用Navigator.pop(context)或者Navigator.of(context).pop()

Navigator.pop方法的定义

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

Navigator.of(context).pop()方法的定义

@optionalTypeArgs
bool pop<T extends Object>([ T result ]) {....}
复制代码

从方法定义上看,两个是等价的,因为Navigator.pop使用的是Navigator.of(context).pop()

1.不需要返回结果给上一个页面时

Navigator.pop(context);
复制代码

2.需要返回结果给上一个页面时

//可以返回任意数据类型
//Navigator.pop(context,"this is result");
//Navigator.pop(context,DateTime.now());
//返回自定义对象
Navigator.pop(context,BlogModel(title:"无题",content: "testtesttest",author: "无名",publishedTime: DateTime.now()));
复制代码

3.Navigator.push携带参数给第二个页面

Navigator.pop有一个可选参数用于返回结果给上一个页面

但是Navigator.push没有可选参数,使用时,也只是创建了一个Widget返回

Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
  return PublishBlogPage();
}));
复制代码

那么怎么传参数给第二个页面?因为Navigator.push调用时,需要创建初始化第二个页面对象,一般来说都是通过构造函数传参,也就是创建第二个页面的对象时,直接赋值,第二个页面就可以直接使用设置的值,以达到跳转时携带参数的目的

Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
  return PublishBlogPage(blogModel: BlogModel(title:"默认标题",content: "这是默认内容"),);
}));
复制代码

4.其他Navigator和Route配合使用的方法

pushReplacement

使用新的Route替换当前Route,当前Route会被销毁,并且可以返回一个result给前一个页面, result是可选的,不赋值默认返回null给前一个页面

@optionalTypeArgs
static Future<T> pushReplacement<T extends Object, TO extends Object>(BuildContext context, Route<T> newRoute, { TO result })
复制代码

pushReplacement使用示例

从页面A,跳转到页面B,页面B调用了pushReplacement把页面B替换成了页面C,并返回值"pushReplacement resule"给页面A

Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context){
  return ThirdTestPage();
}),result:"pushReplacement resule");
复制代码

pushAndRemoveUntil

向栈里添回一个newRoute,并且一个一个地移除之前栈里的Route,直到RoutePredicate方法返回true才停,一般RoutePredicate可以配置route.settings来配合使用

@optionalTypeArgs栈
static Future<T> pushAndRemoveUntil<T extends Object>(BuildContext context, Route<T> newRoute, RoutePredicate predicate)
复制代码

pushAndRemoveUntil使用示例

   Navigator.of(context).pushAndRemoveUntil(
              MaterialPageRoute(builder: (BuildContext context) {
            return CommonPage();
          }), (Route route) {
            //一直关闭,直到首页时停止,停止时,整个应用只有首页和当前页面
            if (route.settings?.name == "/") {
              return true; //停止关闭
            }
            return false; //继续关闭
            //return route==null; //一直关闭页面,直到全部Route都关闭,效果就是整个应用,只剩下当前页面,按返回键会直接回系统桌面
          });
复制代码

popUntil

一个一个地移除之前栈里的Route,直到RoutePredicate方法返回true才停止

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

popUntil使用示例

Navigator.popUntil(context,(Route route){
            print('route$route');
			//到首页时停下,不要销毁首页,
            //如果想整个应用只剩当前页面,可以省略下面逻辑 return route==null; 即可
            if(route?.settings?.name == "/") {
              return true;
            }
            return false;
          });
复制代码

5.命名路由

  命名路由的push方法和普通路由的使用方法大同小异,只是普通路由要创建一个Route来实现页面跳转,命名路由使用一个字符来实现页面跳转,跳转时flutter会在路由表里根据字符串找到要跳转到哪一个页面

命名路由一般都是配合MaterialApp或者CupertinoApp使用

@override
Widget build(BuildContext context) {
return MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: MyHomePage(title: "路由管理"),

  //路由映射
  routes: myRoutes,

  //指定哪个命名路由指向的页面作为首面,这个值生效时上面的home不生效
  initialRoute: "/",

  //在使用命名路由跳转时,如果路由名称没有注册,找不到要跳转到哪里,此方法生效
  onGenerateRoute: (RouteSettings settings) {
    print('onGenerateRoute:$settings');
    if(settings.name== unknowRouteName) {
      return null;
    }
    return MaterialPageRoute(builder: (BuildContext context) {
      return NoFoundPage();
    });
  },

  //在使用命名路由跳转时,如果路由名称没有注册,找不到要跳转到哪里,
  // 并且没有实现onGenerateRoute方法,或者onGenerateRoute方法返回null,此方法生效
  //如果这一个方法也是返回null,则控制台会输出错误信息,这一个方法的返回值不应该为null
  onUnknownRoute: (RouteSettings settings){
    print('onUnknownRoute:$settings');
    return MaterialPageRoute(builder: (BuildContext context) {
      return NoFoundPage();
    });
  },
 );
}
复制代码

命名路由使用规律

当不使用命名路由时,应用使用哪个用页面作为应用首页用home属性配置

当使用routes了注册了路由表时,首页可以使用initialRoute属性配置,"/"是默认的首页,想使用别的页面作为首页时,给initialRoute赋值路由表里相应的路由名称就可以了,当initialRoute生效时home属性不生效

当使用命名路由跳转时,如果路由名称在路由表里找不到,则会先调用onGenerateRoute配置的方法生成一个路由,如果onGenerateRoute配置的方法返回了有效的路由(非null),则显示相应页面,如果onGenerateRoute配置的方法返回的是null,则调用onUnknownRoute配置的方法生成路由显示页面,onUnknownRoute不应该返回null,否则会在控制台输出错误日志

其他方法路由管理方法如何时使用

removeRoute

从Navigator中删除路由,同时执行Route.dispose操作。被删除的路由一定是存在的历史路由,不然抛异常 The given route must be in the history; this method will throw an exception if it is not.

static void removeRoute(BuildContext context, Route<dynamic> route) {
  return Navigator.of(context).removeRoute(route);
}
复制代码

removeRouteBelow

删除指定路由的前一个路由,指定的路由要存在路由线路中,并且指定路由有前一个存在的路由,否则抛异常

举例:路由跳转线路好下 首页->A页面->B页面->C页面 在C页面里有一个按钮,点击时调用removeRouteBelow并指定anchorRoute为跳转到C页面时生成的路由,当按钮点击第一次时B页面被删除,当按钮点击第二次时A页面被删除,当按钮点击第三次时首页被删除,再点击就抛异常了

static void removeRouteBelow(BuildContext context, Route<dynamic> anchorRoute) {
  return Navigator.of(context).removeRouteBelow(anchorRoute);
}	
复制代码

replace

将指定的路由替换成一个新路由。被替换的路由不能是当前可见的,The old route must not be currently visible

@optionalTypeArgs
static void replace<T extends Object>(BuildContext context, { @required Route<dynamic> oldRoute, @required Route<T> newRoute }) {
  return Navigator.of(context).replace<T>(oldRoute: oldRoute, newRoute: newRoute);
}	
复制代码

replaceRouteBelow

将指定的路由的前一个路由替换成新的路由

@optionalTypeArgs
static void replaceRouteBelow<T extends Object>(BuildContext context, { @required Route<dynamic> anchorRoute, Route<T> newRoute }) {
  return Navigator.of(context).replaceRouteBelow<T>(anchorRoute: anchorRoute, newRoute: newRoute);
}
复制代码

以上几个方法的使用,都需要知道路由线路里已经存在的路由才可以调用,Navigator.of(context)得到的NavigatorState对象里有两个成员变量

final List<Route<dynamic>> _history = <Route<dynamic>>[];

final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{};

但是都是私有的,外部无法访问,FlutterSDK也没有提供对外访问的方法。 但是Flutter提供了一个NavigatorObserver导航器观察者,配合MaterialApp或者CupertinoAppnavigatorObservers属性可以实现保存路由线路。自己实现NavigatorObserver保存路由线路,并进行状态管理就可以调用上面的方法了,具体代码请看demo,如果看完这篇博客,还是不太懂,可以运行demo加深印象,加强理解。demo地址在博客开头