前言:
这一章可以看看,没什么难点,主要在路由和入口的讲解,比起vue应该来说要简单不少。
一、概览
- 路由核心模型:Flutter 导航基于路由栈,
Navigator的push/pop是基础操作,pushReplacement/pushAndRemoveUntil是高级栈操作; - 路由管理方式:匿名路由(快速开发)、命名路由(大型应用规范),传参通过
arguments实现; - 界面骨架与导航组件:
Scaffold封装页面结构,AppBar实现顶部导航,BottomNavigationBar实现底部Tab切换,结合路由可完成复杂应用导航; - 高级交互:
WillPopScope拦截返回事件,pushAndRemoveUntil清空路由栈,解决登录/退出等场景的导航逻辑。
在 Flutter 中,所有界面都是 Widget,而路由(Route)就是对页面 Widget 的封装,导航(Navigator)则是管理路由的核心组件。
Flutter 路由管理的核心是路由栈(Route Stack) 模型,底层原理可通过以下结构清晰理解:
- Navigator:Flutter 提供的导航器组件,本质是一个管理路由栈的 Widget,通过静态方法(如
Navigator.push)或上下文调用(Navigator.of(context))操作路由栈。 - 路由栈:遵循“后进先出(LIFO)”原则,栈顶路由对应当前显示的页面,所有跳转操作本质都是对这个栈的增删改。
- 路由对象:封装了页面 Widget 和跳转动画,分为“匿名路由”(直接创建 Route)和“命名路由”(通过名称映射页面)。
- 交互扩展:基于路由栈操作,衍生出传参、拦截、替换、清空栈等高级功能。
二、 核心知识点
2.1 Navigator
核心概念
push:新路由入栈(打开新页面)pop:栈顶路由出栈(返回上一页)
核心属性/方法
| 方法 | 作用 | 注意事项 |
|---|---|---|
Navigator.push | 路由入栈 | 需要传入 BuildContext 和 Route 对象 |
Navigator.pop | 路由出栈 | 无返回值,栈为空时调用会报错 |
MaterialPageRoute | Material 风格的路由封装 | 自带页面切换动画(iOS/Android 适配) |
案例代码
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '路由基础',
home: const FirstPage(),
);
}
}
// 第一个页面
class FirstPage extends StatelessWidget {
const FirstPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: ElevatedButton(
// push 跳转新页面
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondPage(),
),
),
child: const Text('跳转到第二页'),
),
),
);
}
}
// 第二个页面
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('第二页')),
body: Center(
child: ElevatedButton(
// pop 返回上一页
onPressed: () => Navigator.pop(context),
child: const Text('返回首页'),
),
),
);
}
}
注意事项
pop只能返回上一页,无法直接返回到指定页面(需用高级方法);BuildContext必须是 MaterialApp/CupertinoApp 下的上下文,否则找不到 Navigator;- 匿名路由的缺点:页面跳转时需要手动创建 Route,大型应用易冗余。
2.2 命名路由与路由传参
核心概念
- 命名路由:提前在
MaterialApp中注册“路由名称-页面”的映射表,通过名称跳转,简化代码; - 路由传参:跳转时携带数据到目标页面,支持基本类型、对象等。
核心属性/方法
| 方法/属性 | 作用 | 注意事项 |
|---|---|---|
MaterialApp.routes | 注册命名路由映射表 | key 是路由名称(字符串),value 是页面构建函数 |
Navigator.pushNamed | 通过名称跳转路由 | 需确保路由名称已注册,否则报错 |
ModalRoute.of(context)!.settings.arguments | 获取路由参数 | 需做非空判断,参数类型需强转 |
案例代码
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '命名路由与传参',
// 1. 注册命名路由映射表
routes: {
'/': (context) => const HomePage(), // 初始页面
'/detail': (context) => const DetailPage(), // 详情页
},
initialRoute: '/', // 设置初始路由
);
}
}
// 首页(传参方)
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('商品列表')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 2. 命名路由跳转 + 传参(通过 arguments)
Navigator.pushNamed(
context,
'/detail',
arguments: const Product(id: 1001, name: 'Flutter 实战教程'),
);
},
child: const Text('查看商品详情'),
),
),
);
}
}
// 详情页(接收参数)
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
// 3. 获取路由参数
final Product product = ModalRoute.of(context)!.settings.arguments as Product;
return Scaffold(
appBar: AppBar(title: const Text('商品详情')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('商品ID:${product.id}'),
Text('商品名称:${product.name}'),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('返回'),
),
],
),
),
);
}
}
// 数据模型
class Product {
final int id;
final String name;
const Product({required this.id, required this.name});
}
注意事项
- 命名路由建议用
/开头(如/home、/detail),保持规范; - 传参时建议封装数据模型(如上述
Product类),避免直接传零散参数; - 若路由名称未注册,调用
pushNamed会抛出NoSuchMethodError。
2.3 路由替换、清空栈、返回拦截
核心概念
- 路由替换(replace) :用新路由替换栈顶路由(跳转后无法返回原页面);
- 清空栈(pushAndRemoveUntil) :跳转新页面并清空之前的所有路由(如登录后跳首页,禁止返回登录页);
- 返回拦截(WillPopScope) :监听物理返回键/返回按钮,自定义返回逻辑(如提示“是否退出”)。
核心方法/组件
| 方法/组件 | 作用 | 注意事项 |
|---|---|---|
pushReplacement | 替换栈顶路由 | 替换后原栈顶路由被销毁,无法返回 |
pushAndRemoveUntil | 跳转并清空指定路由之前的栈 | 第二个参数是判断条件,(route)=>false 清空所有 |
WillPopScope | 拦截返回事件 | onWillPop 返回 Future<bool>,true 允许返回,false 拦截 |
案例代码
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/login': (context) => const LoginPage(),
'/home': (context) => const HomePage(),
'/profile': (context) => const ProfilePage(),
},
initialRoute: '/login',
);
}
}
// 登录页(演示清空栈)
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('登录页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 登录成功:跳首页 + 清空栈(无法返回登录页)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
(route) => false, // false 表示清空所有之前的路由
);
},
child: const Text('登录并进入首页'),
),
),
);
}
}
// 首页(演示路由替换 + 返回拦截)
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// WillPopScope 拦截返回
return WillPopScope(
onWillPop: () async {
// 弹出确认对话框
final bool? exit = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('提示'),
content: const Text('是否确认退出应用?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('确认')),
],
),
);
return exit ?? false; // 返回 true 则退出,false 则拦截
},
child: Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
// 路由替换:跳个人中心,替换当前首页(无法返回首页)
onPressed: () => Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const ProfilePage()),
),
child: const Text('跳个人中心(替换路由)'),
),
],
),
),
),
);
}
}
// 个人中心
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('个人中心')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('返回(替换路由后无首页可返回)'),
),
),
);
}
}
注意事项
pushAndRemoveUntil常用于登录成功、退出登录等场景,避免用户返回敏感页面;WillPopScope仅拦截物理返回键(Android)和 AppBar 返回按钮,手动调用pop不受影响;- 路由替换/清空栈后,原页面会被销毁,状态丢失(如需保存状态需用
PageStorage)。
2.4 Scaffold、AppBar、BottomNavigationBar
核心概念
Scaffold:Flutter 提供的页面骨架组件,封装了 AppBar、底部导航、抽屉等常见布局;AppBar:页面顶部导航栏,包含标题、返回按钮、操作按钮等;BottomNavigationBar:底部导航栏,用于切换不同页面(结合路由/状态管理)。
核心属性
| 组件 | 核心属性 | 作用 |
|---|---|---|
Scaffold | appBar/body/bottomNavigationBar | 定义页面骨架 |
AppBar | title/actions/leading/centerTitle | 配置顶部导航栏 |
BottomNavigationBar | currentIndex/onTap/items/type | 配置底部导航栏,type 解决多item样式问题 |
案例代码
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '底部导航栏',
home: const MainPage(),
);
}
}
// 主页面(包含底部导航)
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
// 当前选中的底部导航索引
int _currentIndex = 0;
// 底部导航对应的页面
final List<Widget> _pages = const [
HomeTab(),
MessageTab(),
ProfileTab(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
// 顶部导航栏
appBar: AppBar(
title: Text(_getTitle()),
centerTitle: true, // 标题居中
elevation: 2, // 阴影
actions: [
// 右侧操作按钮
IconButton(
icon: const Icon(Icons.search),
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('点击了搜索按钮')),
),
),
],
),
// 页面主体
body: _pages[_currentIndex],
// 底部导航栏
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex, // 当前选中项
onTap: (index) => setState(() => _currentIndex = index), // 切换索引
type: BottomNavigationBarType.fixed, // 固定样式(多于3个item时必须设置)
selectedItemColor: Colors.blue, // 选中颜色
unselectedItemColor: Colors.grey, // 未选中颜色
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
// 根据索引获取标题
String _getTitle() {
switch (_currentIndex) {
case 0:
return '首页';
case 1:
return '消息';
case 2:
return '我的';
default:
return '首页';
}
}
}
// 首页Tab
class HomeTab extends StatelessWidget {
const HomeTab({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
),
child: const Text('跳转到详情页'),
),
);
}
}
// 消息Tab
class MessageTab extends StatelessWidget {
const MessageTab({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('消息页面'));
}
}
// 我的Tab
class ProfileTab extends StatelessWidget {
const ProfileTab({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('我的页面'));
}
}
// 详情页
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('详情页')),
body: const Center(child: Text('这是详情页内容')),
);
}
}
注意事项
BottomNavigationBar当 item 数量 >3 时,必须设置type: BottomNavigationBarType.fixed,否则会自动隐藏label;AppBar的leading默认是返回按钮(有上一级路由时),可自定义覆盖;Scaffold的body高度会自动适配屏幕,无需手动设置。
三、综合应用案例
功能说明
整合本章所有技术,实现一个简易电商APP的导航逻辑:
- 登录页 → 首页(清空栈,禁止返回登录页);
- 首页有底部导航(首页/分类/购物车/我的);
- 首页点击商品 → 详情页(传参,支持返回);
- 详情页点击“加入购物车” → 替换路由到购物车页;
- “我的”页面点击退出登录 → 清空栈返回登录页;
- 物理返回键拦截(首页弹出退出确认)。
完整代码
import 'package:flutter/material.dart';
void main() => runApp(const MyEcommerceApp());
// 全局路由名称常量
class Routes {
static const String login = '/login';
static const String home = '/home';
static const String detail = '/detail';
}
// 商品模型
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
// 应用入口
class MyEcommerceApp extends StatelessWidget {
const MyEcommerceApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter电商APP',
theme: ThemeData(primarySwatch: Colors.blue),
// 注册命名路由
routes: {
Routes.login: (context) => const LoginPage(),
Routes.home: (context) => const MainHomePage(),
Routes.detail: (context) => const ProductDetailPage(),
},
initialRoute: Routes.login, // 初始页为登录页
);
}
}
// 1. 登录页面
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('用户登录'), centerTitle: true),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const TextField(
decoration: InputDecoration(hintText: '请输入用户名'),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
const TextField(
decoration: InputDecoration(hintText: '请输入密码'),
obscureText: true,
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
// 登录成功:跳首页 + 清空栈(无法返回登录页)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const MainHomePage()),
(route) => false,
);
},
style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
child: const Text('登录'),
),
],
),
),
);
}
}
// 2. 首页(包含底部导航)
class MainHomePage extends StatefulWidget {
const MainHomePage({super.key});
@override
State<MainHomePage> createState() => _MainHomePageState();
}
class _MainHomePageState extends State<MainHomePage> {
int _currentTabIndex = 0;
final List<Widget> _tabPages = const [
HomeTab(),
CategoryTab(),
CartTab(),
ProfileTab(),
];
@override
Widget build(BuildContext context) {
// 返回拦截:首页弹出退出确认
return WillPopScope(
onWillPop: () async {
final bool? exit = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('退出确认'),
content: const Text('是否确认退出APP?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('确认')),
],
),
);
return exit ?? false;
},
child: Scaffold(
appBar: AppBar(
title: Text(_getTabTitle()),
centerTitle: true,
actions: _currentTabIndex == 0 ? [
IconButton(icon: const Icon(Icons.search), onPressed: () {}),
] : null,
),
body: _tabPages[_currentTabIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentTabIndex,
onTap: (index) => setState(() => _currentTabIndex = index),
type: BottomNavigationBarType.fixed, // 4个item需设置fixed
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.category), label: '分类'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: '购物车'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
),
);
}
// 根据索引获取Tab标题
String _getTabTitle() {
switch (_currentTabIndex) {
case 0: return '首页';
case 1: return '分类';
case 2: return '购物车';
case 3: return '我的';
default: return '首页';
}
}
}
// 2.1 首页Tab(商品列表)
class HomeTab extends StatelessWidget {
const HomeTab({super.key});
// 模拟商品数据
final List<Product> products = const [
Product(id: 1, name: 'Flutter 实战', price: 59.9),
Product(id: 2, name: 'Dart 入门', price: 49.9),
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('¥${product.price}'),
onTap: () {
// 命名路由跳转 + 传参
Navigator.pushNamed(
context,
Routes.detail,
arguments: product,
);
},
);
},
);
}
}
// 2.2 分类Tab
class CategoryTab extends StatelessWidget {
const CategoryTab({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('分类页面'));
}
}
// 2.3 购物车Tab
class CartTab extends StatelessWidget {
const CartTab({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('购物车页面'));
}
}
// 2.4 我的Tab
class ProfileTab extends StatelessWidget {
const ProfileTab({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
// 退出登录:返回登录页 + 清空栈
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
},
child: const Text('退出登录'),
),
);
}
}
// 3. 商品详情页
class ProductDetailPage extends StatelessWidget {
const ProductDetailPage({super.key});
@override
Widget build(BuildContext context) {
// 获取路由参数
final Product product = ModalRoute.of(context)!.settings.arguments as Product;
return Scaffold(
appBar: AppBar(title: const Text('商品详情')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('商品ID:${product.id}'),
Text('商品名称:${product.name}'),
Text('价格:¥${product.price}'),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
// 路由替换:跳购物车页(替换当前详情页)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const CartTab()),
);
},
child: const Text('加入购物车'),
),
],
),
),
);
}
}
功能验证步骤
- 启动应用 → 进入登录页,点击“登录” → 跳首页(无法返回登录页);
- 首页点击商品 → 进入详情页(携带商品参数);
- 详情页点击“加入购物车” → 跳购物车页(替换路由,无法返回详情页);
- 底部导航切换“我的” → 点击“退出登录” → 返回登录页(清空栈);
- 首页按物理返回键 → 弹出退出确认对话框(拦截返回)。
关键注意事项
- 路由栈操作需避免空栈
pop,建议做非空判断; - 命名路由传参需强转类型,做好异常处理;
- 底部导航栏 item 数量>3 时必须设置
type: fixed; - 清空路由栈常用于登录/退出场景,避免用户返回敏感页面。