🧭 导航组件详解

导航是应用的核心功能,Flutter 提供了强大的导航系统。本教程将详细介绍 Navigator、Router 以及页面跳转的最佳实践。

💡 重要提示:Flutter 3.28+ 引入了 Navigator 2.0,提供了更强大的路由管理和状态管理能力。

🔬 导航原理

Flutter 的导航系统基于路由栈(Route Stack)模型,理解其工作原理对于掌握页面跳转至关重要。

📚 路由栈机制

Navigator 内部维护一个路由栈,遵循后进先出(LIFO)原则:

  • push:将新路由压入栈顶,当前页面被覆盖
  • pop:将栈顶路由弹出,返回上一页
  • pushReplacement:弹出栈顶路由,再压入新路由
  • pushAndRemoveUntil:清空栈中所有路由,再压入新路由

📊 路由栈工作流程

← pop() 弹出栈顶
页面 C(栈顶)
页面 B
页面 A(栈底/首页)
→ push() 压入新页面

🔄 页面切换原理

push 操作

新页面入栈 → 触发 build → 执行动画 → 显示新页面

pop 操作

当前页面出栈 → 触发 dispose → 执行动画 → 显示上一页

replace 操作

先 pop 再 push → 原子操作 → 无返回动画

📞 参数传递原理

正向传递

通过构造函数或 settings.arguments 传递数据给新页面

反向返回

通过 pop(result) 返回数据,await push 接收结果

🎭 动画原理

页面切换动画由PageRouteBuilder的 transitionsBuilder 控制:

PageRouteBuilder(
  pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
  transitionsBuilder: (context, animation, secondaryAnimation, child) {
    // animation: 当前页面的动画控制器(0.0 - 1.0)
    // secondaryAnimation: 下一页进入时的动画控制器
    // child: 要显示的页面 Widget
    
    var tween = Tween(begin: Offset(1.0, 0.0), end: Offset.zero);
    return SlideTransition(
      position: animation.drive(tween),
      child: child,
    );
  },
)
💡 关键点
  • animation 值从 0.0 到 1.0:表示当前页面从开始到结束的动画进度
  • secondaryAnimation:当新页面推入时,当前页面的退出动画
  • child 参数:已经构建好的页面 Widget,直接包装动画即可

🔧 2. 路由构造函数

使用 onGenerateRoute 实现更灵活的路由管理。

📐 onGenerateRoute 路由匹配流程

settings.name = '/'
return MaterialPageRoute(HomePage)

switch 语句匹配路由 → 返回对应的 MaterialPageRoute

MaterialApp(
  onGenerateRoute: (settings) {
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => const HomePage());
      case '/details':
        final id = settings.arguments as int;
        return MaterialPageRoute(
          builder: (_) => DetailsPage(id: id),
        );
      default:
        return MaterialPageRoute(
          builder: (_) => const NotFoundPage(),
        );
    }
  },
);

📑 3. TabBar 和 BottomNavigationBar

实现应用内的标签导航。

📐 TabBar 渲染效果

TabBar 示例
🏠首页
🔍搜索
👤我的
首页内容

顶部标签切换,内容区域联动

TabBar 示例

class TabBarExample extends StatefulWidget {
  const TabBarExample({super.key});

  @override
  State createState() => _TabBarExampleState();
}

class _TabBarExampleState extends State
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TabBar 示例'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(icon: Icon(Icons.home), text: '首页'),
            Tab(icon: Icon(Icons.search), text: '搜索'),
            Tab(icon: Icon(Icons.person), text: '我的'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const [
          HomePage(),
          SearchPage(),
          ProfilePage(),
        ],
      ),
    );
  }
}

📐 BottomNavigationBar 渲染效果

首页内容
🏠
首页
🔍
搜索
👤
我的

底部标签切换,当前项高亮显示

BottomNavigationBar 示例

class BottomNavExample extends StatefulWidget {
  const BottomNavExample({super.key});

  @override
  State createState() => _BottomNavExampleState();
}

class _BottomNavExampleState extends State {
  int _currentIndex = 0;

  final List _pages = [
    const HomePage(),
    const SearchPage(),
    const ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: '搜索',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

🚪 4. Drawer 侧边导航

实现应用内的侧边抽屉导航。

📐 Drawer 渲染效果

主页面内容
👤
用户名
user@example.com
🏠 首页
⚙️ 设置
🚪 退出登录

从屏幕左侧滑出,包含用户信息和导航菜单

Scaffold(
  appBar: AppBar(
    title: const Text('Drawer 示例'),
  ),
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        UserAccountsDrawerHeader(
          accountName: const Text('用户名'),
          accountEmail: const Text('user@example.com'),
          currentAccountPicture: const CircleAvatar(
            backgroundImage: NetworkImage('https://via.placeholder.com/150'),
          ),
          decoration: BoxDecoration(
            color: Theme.of(context).primaryColor,
          ),
        ),
        ListTile(
          leading: const Icon(Icons.home),
          title: const Text('首页'),
          onTap: () {
            Navigator.pop(context);
            Navigator.pushNamed(context, '/');
          },
        ),
        ListTile(
          leading: const Icon(Icons.settings),
          title: const Text('设置'),
          onTap: () {
            Navigator.pop(context);
            Navigator.pushNamed(context, '/settings');
          },
        ),
        ListTile(
          leading: const Icon(Icons.logout),
          title: const Text('退出登录'),
          onTap: () {
            Navigator.pop(context);
            // 退出登录逻辑
          },
        ),
      ],
    ),
  ),
  body: const Center(
    child: Text('从左侧滑出 Drawer'),
  ),
);

✨ 5. Hero 动画

在页面间实现共享元素的过渡动画。

📐 Hero 动画效果

列表页

图片

产品标题

详情页

图片

产品标题

产品描述...

相同 tag 的元素在页面切换时产生平滑过渡动画

// 列表页面
Hero(
  tag: 'product_image',
  child: Image.network(
    'https://example.com/product.jpg',
    width: 100,
    height: 100,
  ),
  onTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const DetailPage()),
    );
  },
)

// 详情页面
Scaffold(
  body: Hero(
    tag: 'product_image',
    child: Image.network(
      'https://example.com/product.jpg',
      width: double.infinity,
      fit: BoxFit.cover,
    ),
  ),
)
技巧:确保 Hero 组件的 tag 在两个页面中完全相同。

🎬 6. 页面过渡动画

自定义页面切换的过渡动画。

📐 页面过渡动画类型

SlideTransition (滑动)

页面A
页面B →

FadeTransition (淡入淡出)

页面A
页面B

ScaleTransition (缩放)

页面B

RotationTransition (旋转)

页面B

使用 PageRouteBuilder 的 transitionsBuilder 自定义动画

// 自定义页面过渡
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) =>
        const SecondPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(1.0, 0.0);
      const end = Offset.zero;
      const curve = Curves.ease;

      var tween = Tween(begin: begin, end: end).chain(
        CurveTween(curve: curve),
      );

      return SlideTransition(
        position: animation.drive(tween),
        child: child,
      );
    },
    transitionDuration: const Duration(milliseconds: 300),
  ),
);

// 淡入淡出效果
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) =>
        const SecondPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: animation,
        child: child,
      );
    },
  ),
);

🔀 页面导航流程图

当前页面
导航方式?
Navigator.push
新页面入栈
Navigator.pop
当前页面出栈
Navigator.pushReplacement
替换当前页面
完成导航

📊 导航方式对比

Flutter 导航方式对比表
方法 栈操作 返回值 使用场景
push 入栈 打开新页面
pop 出栈 返回上一页
pushReplacement 替换 替换当前页
pushAndRemoveUntil 清空 登录后清空栈
pushNamed 入栈 命名路由

📋 导航最佳实践

  • 使用命名路由:集中管理路由,避免硬编码页面路径
  • 路由常量化:将路由路径定义为常量,避免字符串拼写错误
  • 参数类型安全:使用数据模型类传递复杂参数,而不是 Map
  • 错误处理:为未知路由定义 404 页面
  • 页面缓存:使用 AutomaticKeepAliveClientMixin 保持页面状态
  • 转场动画:根据平台选择合适的 PageRoute 类型
  • 路由测试:编写单元测试验证路由跳转逻辑
  • 性能优化:使用 const 构造函数减少重建
🎯 下一步:掌握导航组件后,继续学习对话框组件。