Flutter Route (路由) - 原生路由
前言: Flutter 路由跳转有二种方法:一种是用系统原生的跳转方法。第二种是,用第三方框架...
原生路由
按官方文档原话,我们通常会用“屏”来表示应用的不同页面(界面)。比如,某个应用有一“屏”展示商品列表,当用户点击某个商品的图片,会跳到新的一“屏”展示商品的详细信息。
术语: 在 Flutter 中,屏 (screen) 和 页面 (page) 都叫做 路由 (route),在下文中统称为 “路由 (route)”。
在 Android 开发中,Activity 相当于“路由”,在 iOS 开发中,ViewController 相当于“路由”。在 Flutter 中,“路由”也是一个 Widget。怎么样从一个“路由”跳转到新的“路由“呢?
MaterialPageRoute
MaterialPageRoute
继承自 PageRoute
类,PageRoute
类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute
是 Material 组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
- 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
- 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder
是一个 WidgetBuilder 类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个 widget。我们通常要实现此回调,返回新路由的实例。settings
包含路由的配置信息,如路由名称、是否初始路由(首页)。maintainState
:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState
为 false。fullscreenDialog
表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果fullscreenDialog
为true
,新页面将会从屏幕底部滑入(而不是水平方向)。
Navigator
Navigator
是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator
通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator
提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:
Future push(BuildContext context, Route route)
将给定的路由入栈(即打开新的页面),返回值是一个 Future
对象,用以接收新路由出栈(即关闭)时的返回数据。
bool pop(BuildContext context, [ result ])
将栈顶路由出栈,result
为页面关闭时返回给上一个页面的数据。
Navigator
还有很多其它方法,如 Navigator.replace
、 Navigator.popUntil
等,详情请参考 API 文档或 SDK 源码注释,在此不再赘述。下面我们还需要介绍一下路由相关的另一个概念“命名路由”。
实例方法
Navigator 类中第一个参数为 context 的静态方法都对应一个 Navigator 的实例方法, 比如 Navigator.push(BuildContext context, Route route)
等价于 Navigator.of(context).push(Route route)
,下面命名路由相关的方法也是一样的。
导航到一个新页面和返回
-
创建两个路由
首先,我们来创建两个路由。这是个最简单的例子,每个路由只包含一个按钮。点击第一个路由上的按钮会跳转到第二个路由,点击第二个路由上的按钮,会回退到第一个路由。
class FirstRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('First Route'), ), body: Center( child: RaisedButton( child: Text('Open route'), onPressed: () { // Navigate to second route when tapped. }, ), ), ); } } class SecondRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Second Route"), ), body: Center( child: RaisedButton( onPressed: () { // Navigate back to first route when tapped. }, child: Text('Go back!'), ), ), ); } }
-
用 Navigator.push() 跳转到第二个路由
使用
Navigator.push()
方法跳转到新的路由,push()
方法会添加一个Route
对象到导航器的堆栈上。那么这个Route
对象是从哪里来的呢?你可以自己实现一个,或者直接使用MaterialPageRoute
类。使用MaterialPageRoute
是非常方便的,框架已经为我们实现了和平台原生类似的切换动画。在
FirstRoute
widget 的build()
方法中,我们来修改onPressed()
回调函数:// 位于 FirstRoute widget (Within the `FirstRoute` widget) onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => SecondRoute()), ); }
-
用 Navigator.pop() 回退到第一个路由
怎么关闭第二个路由回退到第一个呢? 使用
Navigator.pop()
方法,pop()
方法会从导航器堆栈上移除Route
对象。我们来修改
SecondRoute
widget 的onPressed()
回调函数,实现返回第一个路由的功能:// 位于 SecondRoute widget (Within the SecondRoute widget) onPressed: () { Navigator.pop(context); }
传递数据到新页面
在开发的过程中,我们经常需要在跳转到新页面的时候,能同时传递一些数据。比如,传递用户点击的元素信息。
-
传递的模型数据类
class Todo { final String title; final String description; Todo(this.title, this.description); }
-
跳转展示的新页面
通过定义变量 Todo 来接收数据
class DetailScreen extends StatelessWidget { // 声明一个成员变量来保存 Todo 对象 final Todo todo; // 构造函数需要 Todo 对象 DetailScreen({Key key, @required this.todo}) : super(key: key); @override Widget build(BuildContext context) { // 使用 Todo 对象构建 UI return Scaffold( appBar: AppBar( title: Text(todo.title), ), body: Padding( padding: EdgeInsets.all(16.0), child: Text(todo.description), ), ); } }
-
原页面
Navigator.push 跳转到新页面,并传入 Todo 模型。
void main() { runApp(MaterialApp( title: 'Passing Data', home: TodosScreen( todos: List.generate( 20, (i) => Todo( 'Todo $i', 'A description of what needs to be done for Todo $i', ), ), ), )); } class TodosScreen extends StatelessWidget { final List<Todo> todos; TodosScreen({Key key, @required this.todos}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Todos'), ), body: ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { return ListTile( title: Text(todos[index].title), onTap: () { // 当用户点击列表时,导航到 DetailScreen, 并当前的 todo 模型传递给它 Navigator.push( context, MaterialPageRoute( builder: (context) => DetailScreen(todo: todos[index]), ), ); }, ); }, ), ); } }
或者使用 RouteSettings 传递参数
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Todo {
final String title;
final String description;
Todo(this.title, this.description);
}
void main() {
runApp(MaterialApp(
title: 'Passing Data',
home: TodosScreen(
todos: List.generate(
20,
(i) => Todo(
'Todo $i',
'A description of what needs to be done for Todo $i',
),
),
),
));
}
class TodosScreen extends StatelessWidget {
final List<Todo> todos;
TodosScreen({Key key, @required this.todos}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todos'),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
onTap: () {
// 当用户点击列表时,导航到 DetailScreen, 并当前的 todo 模型传递给它
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(),
// 作为路由的一部分传递参数。DetailScreen 从这些设置中读取参数。
settings: RouteSettings(
arguments: todos[index],
),
),
);
},
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 取得路由传递参数
final Todo todo = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: AppBar(
title: Text(todo.title),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Text(todo.description),se
),
);
}
}
从一个页面回传数据
在某些场景下,我们需要在回退到上一屏时同时返回一些数据。比如,我们跳转到新的一屏,有两个选项让用户选择,当用户点击某个选项后会返回到第一屏,同时在第一屏可以知道用户选择的信息。可以使用 Navigator.pop()
来进行。
First 页
在 Navigator.push
跳转时,用 await
接收回传的数据,并可以使用 setState
做 UI 更新
import 'package:defensor/views/home/second.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class First extends StatefulWidget {
First({Key key}) : super(key: key);
@override
_FirstState createState() => _FirstState();
}
class _FirstState extends State<First> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First'),
),
body: Container(
child: Center(
child: CupertinoButton(
child: Text('跳转'),
onPressed: () async {
var restult = await Navigator.push(context, MaterialPageRoute(builder: (context) => Second()));
// restult 就是回传的数据
print(restult);
})),
),
);
}
}
Second 页
使用 Navigator.pop(context, data)
回传数据,回传放在第二个参数.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class Second extends StatefulWidget {
Second({Key key}) : super(key: key);
@override
_SecondState createState() => _SecondState();
}
class _SecondState extends State<Second> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second'),
),
body: Container(
child: Center(
child: CupertinoButton(
child: Text('返回并传数据'),
onPressed: () {
// data 传回
var data = '我是传回的数据';
Navigator.pop(context, data);
})),
),
);
}
}
导航到对应名称的 routes (路由表) 里
在 导航到一个新页面和返回 一节中,我们通过创建一个新的路由并将它推到 Navigator
类中学习到了如何导航到新的一个界面 (screen)。
然而,如果我们需要在应用的很多地方导航到同一界面,这样做就会导致代码重复。在这种情况下,定义 命名路由 (named route) 并使用它进行导航就会非常方便。
要使用命名路由,我们可以使用 Navigator.pushNamed()
方法。下面的例子展示如何使用 “命名路由” 来实现前一节中的功能。
创建两个界面
class FirstScreen extends StatelessWidget {
/// 路由名
static const routeName = '/';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Screen'),
),
body: Center(
child: RaisedButton(
child: Text('Launch screen'),
onPressed: () {
// Navigate to the second screen when tapped.
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
/// 路由名
static const routeName = '/second';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Center(
child: RaisedButton(
onPressed: () {
// Navigate back to first screen when tapped.
},
child: Text('Go back!'),
),
),
);
}
}
定义路由
我们需要通过为 MaterialApp
的构造函数额外的属性: initialRoute
和 routes
来定义我们的路由
initialRoute
属性定义了应用应该从哪个路由启动。 routes
属性定义了所有可用的命名路由,以及当我们跳转到这些路由时应该构建的 widgets。
MaterialApp(
// 使用“/”命名路由来启动应用
// 在这里,应用将从 FirstScreen Widget 启动
initialRoute: '/',
routes: {
// 当我们跳转到“/”时,构建 FirstScreen Widget
FirstScreen.routeName: (context) => FirstScreen(),
// 当我们跳转到“/second”时,构建 SecondScreen Widget
SecondScreen.routeName: (context) => SecondScreen(),
},
);
请注意
当使用 initialRoute
时,需要确保你没有同时定义 home
属性。
跳转到第二个界面
准备好了 Widgets 和路由,我们就可以开始进行页面跳转。在这里,我们将使用 Navigator.pushNamed()
函数。它会告诉 Flutter 去构建我们在 routes
表中定义的 widget 并启动该界面。
在 FirstScreen
widget 的 build()
方法中,我们将更新 onPressed()
回调:
// 在 `FirstScreen` Widget中
onPressed: () {
// 使用命名路由跳转到第二个界面
Navigator.pushNamed(context, SecondScreen.routeName);
}
返回到第一个界面
为了能够跳转回第一个页面,我们可以使用 Navigator.pop()
方法。
// 在 SecondScreen Widget 中
onPressed: () {
// 通过从堆栈弹出当前路由
// 来返回到第一个界面
Navigator.pop(context);
}
参考资料:
Flutter 官方中文网 (flutter.cn/docs/get-st…)
Flutter 实战 (book.flutterchina.club/)
转载自:https://juejin.cn/post/6875184031953059853