Flutter的路由与导航,以及页面之间如何共享数据
定义
绝大多数应用应该会由多个不同的界面组成,不同的页面显示不同类型的信息。比如一个应用可以有注册页面,可以有登录页面,可以有用户信息页面,还应该有应用主功能页面,等等。关于路由和导航的定义,我就不咬文嚼字,简单说一下我的理解,在Flutter中,页面和屏也就是用户可见的页面都是路由。在 Android 开发中,Activity 相当于“路由”,在iOS开发中,ViewController 相当于“路由”。在 Flutter 中,“路由”也是一个 widget。现实生活中的导航很容易让人理解,就是出发地如何到目的的交通方式的组合,在应用开发中可以理解为从原页面跳转到目标页面的路径选择过程。业务不复杂,页面比较少的应用,同时没有Deep linking,建议使用Navigator。
导航到一个新页面和返回
怎样从一个“路由”跳转到一个新的“路由”,也就是说页面之间的导航(跳转)会用到 Navigator。路由的下面来展示如何在两个路由间跳转,总共分三步:
- 创建两个路由。
- 用Navigator.push()跳转到第二个路由。
- 用 Navigator.pop()会退到第一个(上一个)路由。 Navigator的底层数据结构应该是使用的栈这个数据结构,栈的特点:先进后出,后进先出。Navigator保存的是页面堆栈数据。Navigator的push方法有两个参数,一个BuildContext,一个Route,源码如下:
Flutter源码有很好的解释说明的文案和方法调用的示例,对我们学习Dart和Flutter有很好的帮助。
pop的方法有两个参数,一个BuildContext,一个返回参数result,源码如下:
pop()方法调用中result是可选的,可以给原来的页面返回数据,同样也可以不返回数据,所以我们做返回数据处理的时候需要对数据做判空处理。Android中的Activity之间传递数据的时候,数据需要实现Parcelable接口,为什么Flutter不需要呢。因为Activity传递数据可能是不同进程之间传递数据,而Flutter是相同进程之间传递数据。
传递数据通过命名式路由
Navigator还有一种命名式的路由,代码示例:
Navigator.pushNamed(context, "/second");
命名式式路由可以处理深度连接,但是行为总是相同而且不能定制。当一个新的深度连接被平台接受以后,Flutter会创建一个新的路由,而不是用原来的路由进行处理,尽管当前路由(页面)是同一类路由。
当使用命名式路由的时候,Flutter也不支持浏览器向前按钮的点击。通过这些原因,我们不建议在绝大多数应用中使用命名式路由。
传递数据到一个新页面
push()方法方法调用过程中使用的Route经常使用MaterialPageRoute这个类。
第二个参数settings的实现,源码如下:
传递参数示例代码如下:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(),
// Pass the arguments as part of the RouteSettings. The
// DetailScreen reads the arguments from these settings.
settings: RouteSettings(
arguments: todo,
),
),
);
Navigator.push()给第二个页面传递数据有两种方式,需要根据自己的业务场景进行选择。
- 直接作为目标页面Widget构造方法中的参数。(直接赋值,页面中可以直接使用)。
- 作为MaterialPageRoute的settings参数的arguments,进行传递。(需要进行获取值并进行转化才可以直接使用)。需要在Widget的build方法中获取参数值,示例代码如下:
final todo = ModalRoute.of(context)!.settings.arguments as Todo;
从一个页面回传数据
从一个页面向原来的页面传递数据需要两个步骤:
- 从退出的页面调用Navigator.pop()方法的时候把数据传递回去,例如 Navigator.pop(context, "Return yes data")。 2.从原来页面需要接受数据并实现相应的业务逻辑,注意push是一个异步方法,调用push方法的时候需要用await关键字。 完整代码如下:
import 'package:flutter/material.dart';
class Todo {
final String title;
final String description;
const 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 {
const TodosScreen({super.key, required this.todos});
final List<Todo> todos;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todos'),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
// When a user taps the ListTile, navigate to the DetailScreen.
// Notice that you're not only creating a DetailScreen, you're
// also passing the current todo through to it.
onTap: () {
_navigateAndDisplaySelection(context, todos[index]);
},
);
},
),
);
}
}
Future<void> _navigateAndDisplaySelection(
BuildContext context, Todo todo) async {
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(),
// Pass the arguments as part of the RouteSettings. The
// DetailScreen reads the arguments from these settings.
settings: RouteSettings(
arguments: todo,
),
),
);
// When a BuildContext is used from a StatefulWidget, the mounted property
// must be checked after an asynchronous gap.
if (!context.mounted) return;
// After the Selection Screen returns a result, hide any previous snackbars
// and show the new result.
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('$result')));
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
@override
Widget build(BuildContext context) {
final todo = ModalRoute.of(context)!.settings.arguments as Todo;
// Use the Todo to create the UI.
return Scaffold(
appBar: AppBar(
title: Text(todo.title),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(todo.description),
ElevatedButton(
onPressed: () {
Navigator.pop(context, "Return yes data");
},
child: const Text("Return yes data")),
ElevatedButton(
onPressed: () {
Navigator.pop(context, "Return no data");
},
child: const Text("Return no data")),
],
),
),
);
}
}
页面之间其他传递数据的方式还有哪些呢
- 全局状态是最简单但可扩展性最差的方法。它涉及使用全局变量来存储数据。代码可测试性和可维护性都不是特别好。
- InheritedWidget允许您有效地在widget树中传播数据。
- Provider是Flutter团队推荐的状态管理解决方案。它可以轻松管理状态和依赖注入。 上面这个三个方案从性能,代码可维护性,可扩展性等方面依次增强,更推荐使用Provider。
总结
随着业务之间的逐渐复杂,不同页面之间的跳转会异常复杂,路由和导航相关的知识已经成为开发过程中不可获取的组成部分,希望文章对您有帮助,如果文中有问题,希望您不吝指教。
转载自:https://juejin.cn/post/7387701265796612132