在真实的应用中,我们通常需要在多个页面之间跳转,比如从首页跳转到详情页、从列表页跳转到设置页等。Flutter 提供了强大的导航系统 Navigator 来管理页面之间的跳转。
本篇教程将带你全面掌握 Navigator 1.0 的核心用法,包括最基础的 push/pop 操作以及更规范的命名路由。
Flutter 将应用中的页面(Screen 或 Page)视为路由(Route) ,并使用一个栈(Stack) 来管理这些路由。Navigator Widget 就是这个路由栈的管理者。
-
基本原理:
Navigator.push(): 将一个新的路由(页面)推入 (push) 导航栈的顶部,用户会看到新页面。Navigator.pop(): 将导航栈顶部的路由弹出 (pop),用户会返回到上一个页面。- 这个"后进先出"(LIFO)的栈结构,完美契合了移动应用中页面跳转和返回的常见模式。
学习目标
- 掌握
Navigator.push()和Navigator.pop()的基本用法,实现页面间的跳转和返回。 - 学会如何在页面跳转时传递数据,以及如何在上一个页面接收返回的数据。
- 理解并掌握命名路由 (Named Routes) 的概念和优势。
- 学会使用
onGenerateRoute来处理带参数的命名路由。
1. 基础导航:push 与 pop
这是最直接、最基础的页面跳转方式。我们通常会用 MaterialPageRoute 来包裹我们的页面 Widget,因为它提供了平台自适应的页面切换动画。
Navigator.push():跳转到新页面
要跳转到一个新页面,你需要调用 Navigator.push(),并提供当前的 BuildContext 和一个 Route 对象(通常是 MaterialPageRoute)。
Navigator.pop():返回上一页
要返回,只需调用 Navigator.pop()。Flutter 的 AppBar 会自动添加一个返回按钮,它的功能就是调用 Navigator.pop()。
数据传递
- 向前传递数据 :在创建新页面的 Widget 时,通过其构造函数传递数据。
- 向后返回数据 :
Navigator.pop()方法可以接受一个可选的参数,作为这个页面的返回值 。同时,Navigator.push()方法会返回一个Future,这个Future会在页面被 pop 时完成,并携带返回值。我们可以使用await来接收它。
代码示例:一个完整的数据传递流程
less
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: FirstScreen(),
);
}
}
// 第一个页面
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
Future<void> _navigateAndDisplaySelection(BuildContext context) async {
// 1. 使用 await 等待 SecondScreen 返回结果
final result = await Navigator.push(
context,
MaterialPageRoute(
// 2. 通过构造函数向前传递数据
builder: (context) => const SecondScreen(data: 'Hello from FirstScreen!'),
),
);
// 5. 接收到返回的数据后,显示一个 SnackBar
if (context.mounted && result != null) {
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('$result')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Screen')),
body: Center(
child: ElevatedButton(
onPressed: () => _navigateAndDisplaySelection(context),
child: const Text('Go to Second Screen'),
),
),
);
}
}
// 第二个页面
class SecondScreen extends StatelessWidget {
final String data;
// 构造函数接收数据
const SecondScreen({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Received data: $data'), // 显示接收到的数据
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 3. Pop 并返回数据 "Yes!"
Navigator.pop(context, 'Yes!');
},
child: const Text('Go back with "Yes!"'),
),
ElevatedButton(
onPressed: () {
// 4. Pop 并返回数据 "No."
Navigator.pop(context, 'No.');
},
child: const Text('Go back with "No."'),
),
],
),
),
);
}
}
2. 命名路由 (Named Routes)
当应用变得复杂,页面众多时,在每个跳转的地方都写一遍 MaterialPageRoute(builder: ...) 会让代码变得混乱且难以管理。命名路由就是为了解决这个问题而生的。
核心思想 :事先给每个页面(路由)起一个独一无二的名字(通常是字符串,如 '/home' 或 '/product/details'),然后将这些名字和对应的页面 Widget 在一个中心位置 (MaterialApp)注册。之后,只需要通过名字就可以进行跳转。
优点:
- 代码整洁 :
Navigator.pushNamed(context, '/details')比Navigator.push(context, MaterialPageRoute(...))简洁得多。 - 集中管理 :所有路由都在
MaterialApp中定义,一目了然,方便维护。 - 解耦:发起跳转的页面不需要知道目标页面的具体实现。
如何实现?
- 在
MaterialApp中定义initialRoute(初始路由) 和routes(路由表)。 - 使用
Navigator.pushNamed(context, routeName)来进行跳转。
如何通过命名路由传递参数?
routes 表定义的 WidgetBuilder 是无参数的,这使得直接传递数据变得困难。为了解决这个问题,我们需要使用更强大的 onGenerateRoute。
onGenerateRoute: 这是MaterialApp的一个回调函数。当pushNamed一个未在routes表中注册 的路由时,Flutter 会调用onGenerateRoute。这给了我们一个拦截路由、解析参数并创建页面的机会。
流程:
- 在
pushNamed时,通过arguments参数传递数据:Navigator.pushNamed(context, '/details', arguments: product)。 - 在
onGenerateRoute回调中,通过settings.arguments获取传递过来的数据。 - 根据路由名和参数,手动创建
MaterialPageRoute并返回。
代码示例:使用命名路由和 onGenerateRoute
kotlin
import 'package:flutter/material.dart';
// 一个简单的数据模型
class Product {
final String title;
final String description;
const Product(this.title, this.description);
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// 1. 定义路由生成规则
onGenerateRoute: (settings) {
// 如果是详情页路由 ('/details')
if (settings.name == ProductDetailsScreen.routeName) {
// 提取参数
final args = settings.arguments as Product;
// 创建并返回 MaterialPageRoute
return MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(product: args);
},
);
}
// 可选:断言确保所有路由都被处理
assert(false, 'Need to implement ${settings.name}');
return null;
},
// 设置首页
home: const HomeScreen(),
);
}
}
// 首页:显示一个产品列表
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final products = List.generate(
20,
(i) => Product('Product ${i + 1}', 'This is the description for product ${i + 1}'),
);
return Scaffold(
appBar: AppBar(title: const Text('Product List')),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(products[index].title),
onTap: () {
// 2. 使用 pushNamed 并通过 arguments 传递数据
Navigator.pushNamed(
context,
ProductDetailsScreen.routeName,
arguments: products[index],
);
},
);
},
),
);
}
}
// 产品详情页
class ProductDetailsScreen extends StatelessWidget {
// 推荐:将路由名定义为静态常量,防止拼写错误
static const routeName = '/details';
final Product product;
const ProductDetailsScreen({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.title)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(product.description),
),
),
);
}
}
总结:如何选择?
-
基础
push/pop:- 优点: 简单直接,易于理解。
- 缺点: 当应用复杂时,路由逻辑分散,难以维护。
- 适用场景: 非常简单的应用,或者页面间的耦合度非常高、不可能被其他地方复用时。
-
命名路由:
- 优点: 集中管理,代码清晰,易于维护,是构建大型应用的基石。
- 缺点: 需要预先定义所有路由,设置上稍微复杂一点。
- 适用场景 : 推荐在绝大多数应用中使用。它是构建可扩展、可维护 Flutter 应用的标准做法。
掌握了 Navigator 1.0 的用法,你就已经能够构建出功能完整的、多页面的 Flutter 应用了。接下来,我们将探讨 Flutter 中一个最核心、也最重要的话题:状态管理,这是构建复杂交互应用的关键。我们下篇见!