路由与对话框
大多数应用程序具有多个页面或视图,并且希望将用户从页面平滑过渡到另一个页面。Flutter的路由和导航功能可帮助您管理应用中屏幕之间的命名和过渡。
可参考官方文档给出的解释 路由和导航
这里我为什么将路由与对话框放在一起呢?因为这两种本质就是同一个东西,我们学习的时候应当注重知识的内在关联。
管理多个页面时有两个核心概念和类:Route和 Navigator。 一个route是一个屏幕或页面的抽象,Navigator是管理route的Widget。Navigator可以通过route入栈和出栈来实现页面之间的跳转
我们可以自己创建导航器,但由于我们通常会使用MaterialApp控件,它的内部已经创建了导航器,因此可以使用Navigator.of来获取其内部的导航器对象。
MaterialApp的home指定的页面就是导航器堆栈底部的路由。要在堆栈上推送(push)一 个新路由,可以创建一个具有构建器功能的MaterialPageRoute对象,并将想要显示的页面在屏幕上展示出来。

MaterialPageRoute是一种模态路由,在不同平台上可以产生不同过渡动画来切换屏幕。对于Android,页面向上滑动过渡面,并将淡入淡出,弹出过渡则向下滑动页面。在iOS上,页面从右侧滑入,反向弹出。
/// 压入一个新路由
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) {
/// 构建一个新的页面
return Container(
child: GestureDetector(
child: Text("确定"),
/// 弹出这个路由
onTap: () =>Navigator.of(context).pop(),
),
);
}
));
动态路由
Flutter中的路由通常可分为动态路由和静态路由,像上面示例中使用了路由构建器生成路由的就属于动态路由,它是动态构建的。
有两种动态路由构建器
PageRouterBuilderMaterialPageRoute
通常使用MaterialPageRoute即可,但当我需要自定义更加丰富的页面切换动画时,则需要使用PageRouterBuilder
/// 创建一个平移动画
SlideTransition createTransition(Animation<double> animation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: child,
);
}
/// 通过PageRouteBuilder 以指定动画跳转新页面
Navigator.of(context).push(
PageRouteBuilder(pageBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
// 返回新页面
return NewPage();
}, transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,) {
return createTransition(animation, child);
}));
静态路由
静态路由也称为命名路由,使用起来更简单,它类似于Web前端中的URL跳转,但它不够灵活,实际项目中的使用并不多。
静态路由的使用分两部分,首先第一部分是注册,类似于配置。
MaterialApp(
title: 'Flutter Demo',
home: HomePage(title: 'Flutter实例'),
routes: <String, WidgetBuilder> {
// 配置路由,用一个类似路径的字符串作键
'/router/second': (_) => SecondPage(),
'/router/profile': (_) => ProfilePage(),
},
);
第二部分就是在合适的地方进行跳转
// 跳转到已经注册的页面
Navigator.of(context).pushNamed('/router/second');
路由相关的有三个属性需要注意
initialRoute: 指定初始路由的名字,即APP启动后应显示的页面onGenerateRoute: 用于生成路由,实际上就是一个匹配器onUnknownRoute: 无法匹配时的回调,类似于Web前端处理的 404 页面
使用中需要注意,initialRoute属性和home属性不应同时设置,它们表达的意义是一样的,只是表现形式不同。如果同时设置了,那么initialRoute的优先级也会高于home
动态路由与静态路由的区别
以上所谓的动态路由和静态路由都只是Flutter中对路由的一种简单封装,以方便开发人员。它们之间最大的区别是,静态路由不能通过页面的构造方法传参,而动态路由可以通过构造方法很方便的将参数传给新页面。另外,如果APP中有太多页面时,使用注册路由表这种方式会显得臃肿而不灵活。
注意,这里有一个常识错误,网络上大量资料说静态路由的缺点是不能传参,实际上是错误的!静态路由只是不能通过构造方法传参而已。
路由传参与返回值
动态路由传参与返回
动态路由中传参给新页面,只需要在构建页面对象的构造方法中传参即可
int _id = 10;
/// 等待 push的返回值
bool result = await Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) {
/// 构建一个新的页面
return NewPage(id:_id);
}
));
print(result);
/// 在新的页面中,只需要退出时在pop中设置返回值即可
Navigator.of(context).pop(false);
通用的传参方式
以上是一种简单方式,假如需要传递的参数的个数不确定,类型不确定,那么使用构造方法传参就有点麻烦,可能需要创建一个列表传过去。这时候就可以使用更高级的传参方式,这种传参方式的好处是可以支持静态路由传参。
Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return NewPage();
},
settings: RouteSettings(
/// 通过一个map来传参
arguments: {'name': 'postbird'},
), // 传参
),
);
在新页面中,只需要如下方式获取参数
class NewPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取参数
Map args = ModalRoute.of(context).settings.arguments;
return Scaffold(
body: Container()
);
}
}
RouteSettings 对象实际上就是路由的基本信息。
匹配器生成路由
MaterialApp(
title: "Demo",
initialRoute: '/',
// 生成匹配的路由
onGenerateRoute: (RouteSettings settings) {
switch(settings.name){
case '/':
return MaterialPageRoute(builder: (context) => HomePage());
case '/secondPage':
return MaterialPageRoute(
/// 这里直接将参数传入页面的构造方法中
builder: (context)=> SecondPage(arguments: settings.arguments));
case '/thirdPage':
return MaterialPageRoute(
builder: (context)=> ThirdPage(arguments: settings.arguments));
default:
return MaterialPageRoute(builder: (context) => HomePage());
}
},
);
在调用的地方,使用命名路由传参。
Navigator.pushNamed(context, "/secondPage",
arguments: {'title': "这是一个标题",'id':0});
接下来我们可以在新页面的构造方法中接收这个map,当然,也可以使用ModalRoute来获取。
需要注意,Navigator执行顺序是 initialRoute => onGenerateRoute => onUnknownRoute
路由栈的操作
Navigator 有以下方法
push将新路由入栈,实现页面跳转pushAndRemoveUntil推送给定路由,删除先前的路由,直到该函数的参数predicate返回true为止Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (BuildContext context) => MyHomePage()), ModalRoute.withName('/'), );pushNamed将命名路由入栈pushNamedAndRemoveUntil同pushAndRemoveUntil,此处为推送命名路由replace将指定路由替换成一个新路由pushReplacement替换指定的路由pushReplacementNamed替换指定的命名路由pop当前路由退栈,实现返回到上个页面canPop判断当前是否可以退栈,如果当前APP在根页面,是不能退栈的maybePop安全的退栈。相当于canPop与pop的组合。popAndPushNamed指定一个路由路径,并导航到新页面。popUntil反复执行pop直到指定的页面为止,即该函数的参数predicate返回true为止removeRoute立即删除路由,同时执行Route.dispose操作。该方法不接受返回值参数,调用是不运行动画的removeRouteBelow删除某指定路由下的路由,同时执行Route.disposeNavigatorState navigator = Navigator.of(context); Navigator.of(context).pop(); Route route = ModalRoute.of(context); /// 删除当前路由下的所有路由 while (navigator.canPop()) navigator.removeRouteBelow(route); /// 推入新路由 await navigator.push( MaterialPageRoute( builder: (BuildContext context) => MyHomePageThree(), ), );replaceRouteBelow将指定路由下的路由替换成一个新路由。需注意,被替换的旧的路由必须是当前不可见的,如果要替换最上面可见的路由,应考虑用pushReplacement
需要注意,pushReplacementNamed和popAndPushNamed在效果上是存在区别的,前者只会显示新路由的进入动画,而后者会显示前一个的退栈动画和新路由的入栈动画。
路由监听
当我们需要监听路由的状态时,可以使用路由监听器。路由监听器可分为全局监听和单页监听
全局监听
自定义类继承自NavigatorObserver,实现需要的回调方法
class CustomObserver extends NavigatorObserver{
@override
void didPush(Route route, Route previousRoute) {
super.didPush(route, previousRoute);
}
@override
void didPop(Route route, Route previousRoute) {
super.didPop(route, previousRoute);
}
}
我们可以在MaterialApp的navigatorObservers属性中设置多个全局监听器
MaterialApp(
navigatorObservers: [CustomObserver()],
)
单页监听
还可以在需要监听的页面中动态注册监听器
CustomObserver customObserver = CustomObserver();
@override
void initState() {
super.initState();
//在initState时动态添加,避免重复添加
Navigator.of(context).widget.observers.add(customObserver);
}
@override
void dispose() {
super.dispose();
//在dispose时移除监听
Navigator.of(context).widget.observers.remove(customObserver);
}
监听器回调
NavigatorObserver类包含以下回调方法
didPushpush 新路由时回调didPop弹出路由时回调didRemove删除路由时回调didReplace新路由替换旧路由时回调didStartUserGesture路由被用户以手势移动时回调,例如iOS中的手势返回didStopUserGesture与didStartUserGesture成对出现,当手势结束时回调
简化监听器
对于简单的路由监听功能,直接继承NavigatorObserver类时仍显得繁琐,因为它包含较多回调方法,这时候我们可以继承用法更简洁的RouteObserver类,实际上该类也是继承自NavigatorObserver,仅仅只是封装了部分逻辑。
class MyPage extends StatefulWidget {
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> with RouteAware{
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
@override
void didPush() {
super.didPush();
}
@override
void didPop() {
super.didPop();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 注册监听
routeObserver.subscribe(this, ModalRoute.of(context));
}
@override
void dispose() {
// 移除监听
routeObserver.unsubscribe(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
只需要在页面的State类上混入RouteAware,并实现需要的回调方法,然后在适当的时候注册监听器即可。官方示例
对话框
对话框是UI界面的一个重要的交互元素,在Flutter中,标准的对话框控件通常就是一个路由。
基本对话框
Material 库提供了三种基本对话框控件
AlertDialog通常用于提示型对话框SimpleDialog通常用于列表型对话框Dialog通常用于自定义布局元素的对话框
弹出对话框时,调用showDialog函数,将对话框控件传入,由于对话框本身是路由,所以关闭对话框时,需使用Navigator.of(context).pop()
除此外,还有一种iOS风格的基本对话框控件 CupertinoAlertDialog,用法与之类似,使用showCupertinoDialog函数显示,需要注意导入cupertino的包
AlertDialog 示例
InkWell(
onTap: (){
showDialog(context: context,child: AlertDialog(
title: Text("提示"),
content: Text("这是一个对话框控件"),
actions: <Widget>[
FlatButton(
child: Text("取消"),
//关闭对话框
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text("知悉"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
));
},)
SimpleDialog 示例
InkWell(
onTap: () async{
int i = await showDialog<int>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('请选择语言'),
children: <Widget>[
SimpleDialogOption(
onPressed: () {
Navigator.pop(context, 1);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: const Text('中文简体'),
),
),
SimpleDialogOption(
onPressed: () {
Navigator.pop(context, 2);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: const Text('中文繁体'),
),
),
],
);
});
print("选择了$i");
},)
需要注意,AlertDialog和SimpleDialog中不能直接使用如ListView、GridView 、 CustomScrollView等控件,必须给它们包裹一层设置固定的高度,否则会报错。这时候,我们就可以使用Dialog来解决问题
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: ListView(
children: <Widget>[
Text("item 1"),
Text("item 2"),
],
),
);
});
模态对话框
使用showModalBottomSheet函数可以弹出模态对话框,与基本对话框一样,它本质上也是一个路由。
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return ListView.builder(
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: () => Navigator.of(context).pop(),
);
},
);
},
);
当然,Flutter也提供了一个iOS风格的模态对话框
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) {
return CupertinoActionSheet(
title: Text('提示'),
message: Text('这是一个iOS风格的模态对话框'),
actions: <Widget>[
CupertinoActionSheetAction(
child: Text('确定'),
onPressed: () {},
isDefaultAction: true,
),
CupertinoActionSheetAction(
child: Text('暂时取消'),
onPressed: () {},
isDestructiveAction: true,
),
],
);
}
);
底部弹框
使用showBottomSheet 函数可以弹出一个底部弹框,这里需要特别声明,实际上showBottomSheet 弹出的并不是一个对话框,只是一个底部弹框,虽然它的名字和showModalBottomSheet相似,但两者有本质区别,这里极容易混淆。
调用该函数会从设备底部向上弹出一个菜单列表
PersistentBottomSheetController _controller;
_controller = showBottomSheet(
context: context,
builder: (BuildContext context) {
return SizedBox(
height: 250,
child: ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: (){
// 关闭弹框
_controller.close();
},
);
},
),
);
},
);
两者的区别
showBottomSheet实际上是Scaffold中的方法,必须在Scaffold下使用,且必须获得Scaffold的contextshowBottomSheet和 页面框架的悬浮按钮fab存在组合动画showModalBottomSheet是一个对话框路由,因此会占满全屏,未占用的空间会显示半透明蒙层
其他对话框
showAboutDialog关于对话框showDatePicker日期选择器showTimePicker时间选择器showCupertinoModalPopup+CupertinoTimerPickeriOS 风格时间选择器showCupertinoModalPopup+CupertinoDatePickeriOS 风格日期选择器
示例
var date = DateTime.now();
Future<DateTime> result = showCupertinoModalPopup(
context: context,
builder: (ctx) {
return SizedBox(
height: 300,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
minimumDate: date,
maximumDate: date.add(
Duration(days: 30),
),
maximumYear: date.year + 1,
onDateTimeChanged: (DateTime value) {
print(value);
},
),
);
},
);
公众号“编程之路从0到1”