第十一讲 界面导航与路由管理

0 阅读11分钟

前言:

这一章可以看看,没什么难点,主要在路由和入口的讲解,比起vue应该来说要简单不少。

一、概览

  1. 路由核心模型:Flutter 导航基于路由栈Navigatorpush/pop 是基础操作,pushReplacement/pushAndRemoveUntil 是高级栈操作;
  2. 路由管理方式:匿名路由(快速开发)、命名路由(大型应用规范),传参通过 arguments 实现;
  3. 界面骨架与导航组件Scaffold 封装页面结构,AppBar 实现顶部导航,BottomNavigationBar 实现底部Tab切换,结合路由可完成复杂应用导航;
  4. 高级交互WillPopScope 拦截返回事件,pushAndRemoveUntil 清空路由栈,解决登录/退出等场景的导航逻辑。

在 Flutter 中,所有界面都是 Widget,而路由(Route)就是对页面 Widget 的封装,导航(Navigator)则是管理路由的核心组件。

Flutter 路由管理的核心是路由栈(Route Stack) 模型,底层原理可通过以下结构清晰理解:

image.png

  1. Navigator:Flutter 提供的导航器组件,本质是一个管理路由栈的 Widget,通过静态方法(如 Navigator.push)或上下文调用(Navigator.of(context))操作路由栈。
  2. 路由栈:遵循“后进先出(LIFO)”原则,栈顶路由对应当前显示的页面,所有跳转操作本质都是对这个栈的增删改。
  3. 路由对象:封装了页面 Widget 和跳转动画,分为“匿名路由”(直接创建 Route)和“命名路由”(通过名称映射页面)。
  4. 交互扩展:基于路由栈操作,衍生出传参、拦截、替换、清空栈等高级功能。

二、 核心知识点

2.1 Navigator

核心概念
  • push:新路由入栈(打开新页面)
  • pop:栈顶路由出栈(返回上一页)
核心属性/方法
方法作用注意事项
Navigator.push路由入栈需要传入 BuildContextRoute 对象
Navigator.pop路由出栈无返回值,栈为空时调用会报错
MaterialPageRouteMaterial 风格的路由封装自带页面切换动画(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('返回首页'),
        ),
      ),
    );
  }
}

注意事项
  1. pop 只能返回上一页,无法直接返回到指定页面(需用高级方法);
  2. BuildContext 必须是 MaterialApp/CupertinoApp 下的上下文,否则找不到 Navigator;
  3. 匿名路由的缺点:页面跳转时需要手动创建 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});
}

注意事项
  1. 命名路由建议用 / 开头(如 /home/detail),保持规范;
  2. 传参时建议封装数据模型(如上述 Product 类),避免直接传零散参数;
  3. 若路由名称未注册,调用 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('返回(替换路由后无首页可返回)'),
        ),
      ),
    );
  }
}
注意事项
  1. pushAndRemoveUntil 常用于登录成功、退出登录等场景,避免用户返回敏感页面;
  2. WillPopScope 仅拦截物理返回键(Android)和 AppBar 返回按钮,手动调用 pop 不受影响;
  3. 路由替换/清空栈后,原页面会被销毁,状态丢失(如需保存状态需用 PageStorage)。

2.4 Scaffold、AppBar、BottomNavigationBar

核心概念
  • Scaffold:Flutter 提供的页面骨架组件,封装了 AppBar、底部导航、抽屉等常见布局;
  • AppBar:页面顶部导航栏,包含标题、返回按钮、操作按钮等;
  • BottomNavigationBar:底部导航栏,用于切换不同页面(结合路由/状态管理)。
核心属性
组件核心属性作用
ScaffoldappBar/body/bottomNavigationBar定义页面骨架
AppBartitle/actions/leading/centerTitle配置顶部导航栏
BottomNavigationBarcurrentIndex/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('这是详情页内容')),
    );
  }
}

注意事项
  1. BottomNavigationBar 当 item 数量 >3 时,必须设置 type: BottomNavigationBarType.fixed,否则会自动隐藏label;
  2. AppBarleading 默认是返回按钮(有上一级路由时),可自定义覆盖;
  3. Scaffoldbody 高度会自动适配屏幕,无需手动设置。

三、综合应用案例

功能说明

整合本章所有技术,实现一个简易电商APP的导航逻辑:

  1. 登录页 → 首页(清空栈,禁止返回登录页);
  2. 首页有底部导航(首页/分类/购物车/我的);
  3. 首页点击商品 → 详情页(传参,支持返回);
  4. 详情页点击“加入购物车” → 替换路由到购物车页;
  5. “我的”页面点击退出登录 → 清空栈返回登录页;
  6. 物理返回键拦截(首页弹出退出确认)。

完整代码

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('加入购物车'),
            ),
          ],
        ),
      ),
    );
  }
}

功能验证步骤

  1. 启动应用 → 进入登录页,点击“登录” → 跳首页(无法返回登录页);
  2. 首页点击商品 → 进入详情页(携带商品参数);
  3. 详情页点击“加入购物车” → 跳购物车页(替换路由,无法返回详情页);
  4. 底部导航切换“我的” → 点击“退出登录” → 返回登录页(清空栈);
  5. 首页按物理返回键 → 弹出退出确认对话框(拦截返回)。

关键注意事项

  • 路由栈操作需避免空栈 pop,建议做非空判断;
  • 命名路由传参需强转类型,做好异常处理;
  • 底部导航栏 item 数量>3 时必须设置 type: fixed
  • 清空路由栈常用于登录/退出场景,避免用户返回敏感页面。