路由管理
FLutter中的路由,和原生组件化的路由一样,就是页面之间的跳转,也可以称之为导航。app维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
MaterialPageRoute
MaterialPageRoute是一种模态路由,可以针对不同平台自适应的过渡动画替换整个屏幕页面:
对于Android,打开新页面时,新页面从屏幕底部导入到顶部。关闭页面的时候,会从顶部滑动到底部消失。
在iOS上,页面从右侧滑入并反向退出。
下面我们介绍一下MaterialPageRoute 构造函数的各个参数的意义:
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。
settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。
maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为false。
fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在iOS中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)
基本使用
Flutter为我们提供了导航器Navigator。参数传入当前的BuildContext和要导航的页面即可。
- 调用Navigator.push导航到第二个页面
Navigator.push( context, new MaterialPageRoute(builder: (context) => Page2()));
- 调用Navigator.pop返回前一个页面
Navigator.pop(context, result);
-
关闭页面后获取结果
有时候我们需要上个页面关闭时传递一个返回值,幸运的是,Navigator的调用方法都是Future,因此我们可以等待它们的结果:3.1. 等待Navigator运行
3.2. 将返回值传递给Navigator.pop函数
3.3. 等待完成后,获取返回值在page1中,导航到page2,并且await到page2传递返回值并pop,根据返回值弹出不同的对话框:
onPressed: () async { var navigationResult = await Navigator.push( context, new MaterialPageRoute(builder: (context) => Page2())); if (navigationResult == 'from_back') { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Navigation from back'), )); } else if (navigationResult == 'from_button') { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Navigation from button'), )); } },
在page2中传递返回值并返回:
Navigator.pop(context, 'from_button');
-
拦截返回键
如果不想点击返回键关闭当前页面,可以使用WillPopScope小部件,用它放在最外层包括住脚手架。并向onWillPop返回false。false告诉系统当前页面不处理返回。
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => Future.value(false),
child: Scaffold(
body: Container(
child: Center(
child: Text('Page 2',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
),
),
),
);
}
如果想自定义处理返回键,可以在return false 之前自己处理,比如关闭 当前页面并传递返回值:
WillPopScope(
onWillPop: () async {
// You can await in the calling widget for my_value and handle when complete.
Navigator.pop(context, 'my_value');
return false;
},
...
);
命名路由
基本使用
上面代码是在没个需要导航的地方声明路由,不能复用,我们可以先给路由起一个名字,再注册路由表,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式,并且可以复用。
MaterialApp的routes属性,既是注册路由表用的,它对应一个Map<String, WidgetBuilder>。
起名:
static const String page1 = "/page1";
static const String page2 = "/page2";
声明路由表:
Map<String, WidgetBuilder> routes = {
page1: (context) => Page1(),
page2: (context) => Page2(),
};
注册路由表:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Page1(),
);
}
然后在需要路由的地方使用命名路由调用:
Navigator.pushNamed(context, page2)
传递参数
给page3起名:
page3: (context) => Page3(),
打开路由时候传递参数:
Navigator.of(context).pushNamed(page3, arguments: "hi");
page3中接收参数:
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
//获取路由参数
var args = ModalRoute.of(context).settings.arguments;
return Scaffold(
body: Container(
child: Center(
child: Text('Page 3的参数是$args',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
),
),
);
}
}
构造函数传参
上面我们明明给page3传递了参数,但是并非传递到构造函数上。我们看构造函数,并不知道传递了什么参数,必须去看路由,并不是很好的做法。那怎么给构造函数传参呢?
起名:
const String page4 = "/page4";
注册路由:
page4: (context) => Page4(text: ModalRoute.of(context).settings.arguments),
打开路由时传递参数:
Navigator.of(context).pushNamed(page4, arguments: "hello");
动态路由
MaterialApp还为我们提供了一个onGenerateRoute参数,未在路由表里注册的路由,会在这里寻找。RouteFactory有一个RouteSettings参数,并返回一个Route<dynamic>。这是我们将用来执行所有路由的功能。
Route<dynamic> Function(RouteSettings settings)
我们可以这样使用:
先声明路由表:
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case page5:
return MaterialPageRoute(builder: (context) => Page5());
case page6:
return MaterialPageRoute(builder: (context) => Page6());
default:
return MaterialPageRoute(builder: (context) => Page1());
}
注册:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
onGenerateRoute: generateRoute,
home: Page1(),
);
}
打开路由:
Navigator.of(context).pushNamed(page5);
动态路由传递参数
上面说了,settings可以拿到参数,我们当然就可以传递参数了:
Route<dynamic> generateRoute(RouteSettings settings) {
print('====${settings.name}');
switch (settings.name) {
case page5:
return MaterialPageRoute(builder: (context) => Page5());
case page6:
return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
default:
return MaterialPageRoute(builder: (context) => Page1());
}
}
使用:
Navigator.of(context).pushNamed(page6, arguments: "world");
so easy。
处理未定义的路线
有两种处理未定义路由的方法。
- 利用generateRoute,找不到路由名的返回默认路由
Route<dynamic> generateRoute(RouteSettings settings) {
print('====${settings.name}');
switch (settings.name) {
case page5:
return MaterialPageRoute(builder: (context) => Page5());
case page6:
return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
default:
return MaterialPageRoute(builder: (context) => NotFindPage());
}
}
- 利用onUnknownRoute返回默认路由
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
onGenerateRoute: generateRoute,
onUnknownRoute: (settings) =>
MaterialPageRoute(builder: (context) => NotFindPage()));
}
初始路由
打开应用第一屏的路由,也有2种方式,
- 可以设置initialRoute,指定路由表里注册的路由名。
- 可以设置home,对应的page。
initialRoute会覆盖home。
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: root,
home: Page2(),
onGenerateRoute: generateRoute,
onUnknownRoute: (settings) =>
MaterialPageRoute(builder: (context) => NotFindPage()));
}
不使用BuildContext的路由导航
很多情况是,我们已将UI代码从业务逻辑中分离出来(类似于MVVM架构)。viewModel应处理所有逻辑,视图应仅调用模型上的函数,然后在需要时使用新状态重建自身。
我们知道Navigator需要BuildContext的参数,我们在进行实际业务逻辑决策的位置进行导航,而不是在widget里调用路由,如果在viewModel里导航,就要传入context吗?下面实现不要context的导航。
为了遵守MVVM原则,我们将把Navigation功能移动到可以从viewModel调用的服务中。在lib下创建一个名为services的新文件夹,并在其中创建一个名为navigation_service.dart的新文件。
先实现单利模式:
class NavigationService {
factory NavigationService.getInstance() => _getInstance();
NavigationService._internal();
static NavigationService _instance;
static NavigationService _getInstance() {
if (_instance == null) {
_instance = new NavigationService._internal();
}
return _instance;
}
}
然后利用navigatorKey实现:
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState.pushNamed(routeName);
}
void goBack() {
return navigatorKey.currentState.pop();
}
我们将NavigationService与应用程序链接的方式,通过navigatorKey提供给MaterialApp。转到main.dart文件并设置navigatorKey:
MaterialApp(
title: 'Flutter Demo',
navigatorKey: NavigationService().navigatorKey,
...
)
然后写一个viewModel,尝试导航:
class ViewModel {
final NavigationService _navigationService = NavigationService();
Future goPage1() async{
/// 模拟请求数据后调到首页
await Future.delayed(Duration(seconds: 1));
_navigationService.navigateTo(page1);
}
}
在page6里使用viewModel导航:
onPressed: () {
viewModel.goPage1();
},
现在,将View文件的职责带回到了“显示UI”并将用户操作传递给模型,而不是“显示UI”将用户操作传递给模型并进行导航。更符合MVVM职责的划分。
这样做的好处是,随着导航逻辑的扩展,我们的UI将保持不变,并且模型将承载所有逻辑/状态管理。
网友评论