往期回顾
从零开始的 Flutter 之旅: StatelessWidget
从零开始的 Flutter 之旅: StatefulWidget
从零开始的 Flutter 之旅: InheritedWidget
从零开始的 Flutter 之旅: Provider
这篇文章是从零开始系列的第五期,前面我们讲到了 Widget 与结合数据共享的 Provider 处理。
这次我们接着来了解一下路由导航 Navigator 的相关信息。
Flutter 中的路由管理与原生开发类似,都会维护一个路由栈,通过 push 入栈打开一个新的页面,然后再通过 pop 出栈关闭老的页面。
示例
我们直接到 flutter_github 中找个简单的实例。
void _goToLogin() async {SharedPreferences prefs = await SharedPreferences.getInstance();
String authorization = prefs.getString(SP_AUTHORIZATION);
String token = prefs.getString(SP_ACCESS_TOKEN);
if ((authorization != null && authorization.isNotEmpty) ||
(token != null && token.isNotEmpty)) {Navigator.of(context).push(MaterialPageRoute(builder: (context) {return HomePage();
}));
} else {Navigator.of(context).push(MaterialPageRoute(builder: (context) {return LoginPage();
}));
}
}
上面的方法是判断是否已经登录了。如果登录了通过过 Navigator 跳转到 HomePage 页面,否则跳转到 LoginPage 页面。
用法很简单通过 push 传递一个 Route。这里对应的是 MaterialPageRoute。它会提供一个 builder 方法,我们直接在 builder 中返回想要跳转的页面实例即可。
它继承于 PageRoute,PageRoute 是一个抽象类,它提供了路由切换时的过渡动画效果与相应的接口。而 MaterialPageRoute 通过这些接口来实现不同平台上对应风格的路由切换动画效果。例如:
- Android 平台,push 时页面会从屏幕底部滑动到顶部进入,pop 时页面会从屏幕顶部滑动到屏幕底部退出。
- Ios 平台,push 时页面会从屏幕右侧滑动到屏幕左侧进入,pop 时页面会从屏幕左侧滑动到屏幕右侧退出。
如果想自定义切换动画,可以仿照 MaterialPageRoute,继承于 PageRoute 来实现。
Navigator
需要注意的是,push 操作会返回一个 Future,它是用来接收新的路由关闭时返回的数据。在 Android 中对应的就是 startActivityForResult() 和 onActivityResult()API。
@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {assert(!_debugLocked);
assert(() {
_debugLocked = true;
return true;
}());
....
....
}
对应的另一个是 pop 操作,出栈是可以向之前的页面传递数据,在 Android 中对应的就是 setResult() Api
@optionalTypeArgs
bool pop<T extends Object>([T result]) {assert(!_debugLocked);
assert(() {
_debugLocked = true;
return true;
}());
final Route<dynamic> route = _history.last;
assert(route._navigator == this);
bool debugPredictedWouldPop;
...
...
}
除了上面两个常用的,还有下面几个特殊的操作
- pushReplacement: 将当前的路由页面进行替换成新的路由页面, 之前的路由将会失效。
- pushAndRemoveUntil: 加入一个新的路由,同时它接收一个判断条件,如果满足条件将会移除之前所有的路由。
这些都是根据特定场景使用,例如文章最开始的登录判断示例。这段判断代码其实在 App 启动时的引导页面中,所以不管最终跳转到哪个页面,最终这个引导页面都需要从路由中消失,所以这里就可以通过 pushReplacement 来开启新的路由页面。
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context){return HomePage();
}));
传参
路由跳转页面自然少不了参数的传递,通过上面的方式进行路由跳转,传参也非常简单,可以直接通过实例类进行传参。
我这里以 flutter_github 中的 WebViePage 为例。
class WebViewPage extends BasePage<_WebViewState> {
final String url;
final String requestUrl;
final String title;
WebViewPage({@required this.title, this.url = '', this.requestUrl =''});
@override
_WebViewState createBaseState() => _WebViewState(title, url, requestUrl);
}
上面是 WebViewPage 参数的接收,直接通过实例化进行参数传递
contentTap(int index, BuildContext context) {NotificationModel item = _notifications[index];
if(item.unread) _markThreadRead(index, context);
Navigator.push(context, MaterialPageRoute(builder: (_) {
return WebViewPage(
title: item.subject?.title ?? '',
requestUrl: item.subject?.url ?? '',
);
}));
}
这里是通过点击文本跳转到 WebViewPage 页面,使用 push 操作来导航到 WebViewPage 页面,同时在实例化时将相应的参数传递过去。
以上是相对比较原始的方法进行参数传递,还有另一种
做个 Android 的朋友都知道在 Activity 页面跳转时可以同 Intent 进行参数传递,而接受页面也可以通过 Intent 来获取传递过来的参数。
在 Flutter 中也有类似的传参方式。我们可以通过 MaterialPageRoute 中的 settings 来构建一个 arguments 对象,将其传递到跳转的页面中。
将上面的代码进行改版
contentTap(int index, BuildContext context) {NotificationModel item = _notifications[index];
if (item.unread) _markThreadRead(index, context);
Navigator.push(
context,
MaterialPageRoute(builder: (_) {return WebViewPage();
},
settings: RouteSettings(arguments: {WebViewPage.ARGS_TITLE: item.subject?.title ?? '', WebViewPage.ARGS_REQUEST_URL: item.subject?.url ??''})));
}
这是参数传递,下面是 WebViewPage 中对参数的接收处理
Map<String, String> arguments = ModalRoute.of(context).settings.arguments;
_title = arguments[WebViewPage.ARGS_TITLE];
_url = arguments[WebViewPage.ARGS_URL];
vm.requestUrl = arguments[WebViewPage.ARGS_REQUEST_URL];
在接收页面参数是通过 ModalRoute 来获取的,获取到的 arguments 就是上面传递过来的参数 map 数据。
ModalRoute.of() 内部运用的是 context.dependOnInheritedWidgetOfExactType()
是不是有点眼熟?如果不记得的话推荐重新温习一遍从零开始的 Flutter 之旅: InheritedWidget
以上都是非命名路由,下面我们再来了解一下命名路由的使用与参数方式。
命名路由
命名路由,顾名思义通过提前注册好的名称来跳转到对应的页面。
首页我们需要注册一个路由表,约定好名称与页面的一一对应。
而路由表可以通过 routes 来定义
final Map<String, WidgetBuilder> routes;
通过定义,应该很好理解。它是一个 map,key 代表路由名称,value 代表具体的页面实例。
以 flutter_github 中的 GithubApp 为例。
class _GithubAppState extends State<GithubApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Github',
theme: ThemeData.light(),
initialRoute: welcomeRoute.routeName,
routes: {welcomeRoute.routeName: (BuildContext context) => WelcomePage(),
loginRoute.routeName: (BuildContext context) => LoginPage(),
homeRoute.routeName: (BuildContext context) => HomePage(),
repositoryRoute.routeName: (BuildContext context) => RepositoryPage(),
followersRoute.routeName: (BuildContext context) =>
FollowersPage(followersRoute.pageType),
followingRoute.routeName: (BuildContext context) =>
FollowersPage(followingRoute.pageType),
webViewRoute.routeName: (BuildContext context) => WebViewPage(),},
);
}
}
需要注意的有两点
- initialRoute 是用来初始化路由页面,它接收的也是对应路由页面的注册名称
- routes 就是注册的路由表,只需通过 key、value 的方式来注册对应的路由页面。
为了方便管理路由的跳转,这里使用了 AppRoutes 来统一管理路由的名称
class AppRoutes {
final String routeName;
final String pageTitle;
final String pageType;
const AppRoutes(this.routeName, {this.pageTitle, this.pageType});
}
class PageType {
static const String followers = 'followers';
static const String following = 'following';
}
const AppRoutes welcomeRoute = AppRoutes('/');
const AppRoutes loginRoute = AppRoutes('/login');
const AppRoutes homeRoute = AppRoutes('/home');
const AppRoutes repositoryRoute =
AppRoutes('/repository', pageTitle: 'repository');
const AppRoutes followersRoute = AppRoutes('/followers',
pageTitle: 'followers', pageType: PageType.followers);
const AppRoutes followingRoute = AppRoutes('/following',
pageTitle: 'following', pageType: PageType.following);
const AppRoutes webViewRoute = AppRoutes('/webview', pageTitle: 'WebView');
现在我们已经注册好了需要跳转的页面路由,接下来使用命名路由的方式来替换之前介绍的路由方式。
void _goToLogin() async {SharedPreferences prefs = await SharedPreferences.getInstance();
String authorization = prefs.getString(SP_AUTHORIZATION);
String token = prefs.getString(SP_ACCESS_TOKEN);
if ((authorization != null && authorization.isNotEmpty) ||
(token != null && token.isNotEmpty)) {Navigator.pushReplacementNamed(context, homeRoute.routeName);
} else {Navigator.pushReplacementNamed(context, loginRoute.routeName);
}
}
在登录状态判断跳转的过程中,可以直接通过 pushReplacementNamed() 来跳转到对应的页面。与之前的区别是,我们只需传递对应跳转页面的路由名称。因为已经有了路由注册表,所以会自己转变成相应的页面。
对应的方法还有 pushNamed() 与 pushNamedAndRemoveUntil()
对于命名路由的参数传递与之前最后面介绍的参数传递方式类似,例如
Navigator.of(context).pushNamed(webViewRoute.routeName,
arguments: {WebViewPage.ARGS_TITLE: item.subject?.title ?? '', WebViewPage.ARGS_REQUEST_URL: item.subject?.url ??''});
基本上类似,也是传递一个 arguments,对应的页面接收参数的方式保存不变。
onGenerateRoute
命名路由中还有一个需要注意的是 onGenerateRoute
class _GithubAppState extends State<GithubApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
onGenerateRoute: (RouteSettings setting) {return MaterialPageRoute(builder: (context) {
String routeName = setting.name;
// todo navigator
});
},
);
}
}
它的回调条件是:跳转的页面没有在 routes 中进行路由注册
通过该回调方法,我们可以在这里进行路由拦截,再统一做一些页面跳转的逻辑处理。
Navigator 方面的知识就介绍到这里,如果文章中有不足的地方欢迎指出,或者说你这其中有什么疑问也可以留言与我,我将力所能及的进行解答。
推荐项目
下面介绍一个完整的 Flutter 项目,对于新手来说是个不错的入门。
flutter_github,这是一个基于 Flutter 的 Github 客户端同时支持 Android 与 IOS,支持账户密码与认证登陆。使用 dart 语言进行开发,项目架构是基于 Model/State/ViewModel 的 MSVM;使用 Navigator 进行页面的跳转;网络框架使用了 dio。项目正在持续更新中,感兴趣的可以关注一下。
当然如果你想了解 Android 原生,相信 flutter_github 的纯 Android 版本 AwesomeGithub 是一个不错的选择。
如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,建议您关注我的微信公众号:【Android 补给站】
或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。