← Frontend / Flutter

09_路由与导航

Flutter 路由管理和页面导航

Flutter 的路由基于栈结构,Navigator 对应 Vue Router / React Router。


基本概念

Flutter 的路由 = 页面栈。push 进入新页面,pop 返回上一页,类似浏览器的前进后退。


基本导航

// push:跳转到新页面(类似 router.push('/detail'))
Navigator.of(context).push(
  MaterialPageRoute(          // Android 风格:从下向上滑入
    builder: (context) => DetailPage(id: 123),
  ),
);

// CupertinoPageRoute:iOS 风格:从右向左滑入
Navigator.of(context).push(
  CupertinoPageRoute(builder: (_) => DetailPage(id: 123)),
);

// pop:返回上一页(类似 router.back())
Navigator.of(context).pop();

// pop 并携带返回值
Navigator.of(context).pop({'result': 'success'});

// 接收 pop 的返回值(await push)
final result = await Navigator.of(context).push(
  MaterialPageRoute(builder: (_) => const EditPage()),
);
if (result != null && mounted) {
  print('编辑结果: $result');
}

常用 Navigator 方法

// pushReplacement:替换当前页(不能返回,类似 router.replace)
// 登录成功跳首页,不允许返回登录页
Navigator.of(context).pushReplacement(
  MaterialPageRoute(builder: (_) => const HomePage()),
);

// pushAndRemoveUntil:清空栈再跳转(退出登录 → 登录页)
Navigator.of(context).pushAndRemoveUntil(
  MaterialPageRoute(builder: (_) => const LoginPage()),
  (route) => false,           // false = 清空所有历史
);

// popUntil:连续返回到指定页面
Navigator.of(context).popUntil(ModalRoute.withName('/home'));

// canPop:是否可返回(自定义返回逻辑)
if (Navigator.of(context).canPop()) {
  Navigator.of(context).pop();
}

传参方式

方式一:构造函数传参(推荐,简单场景)

// 直接传给目标页构造函数
Navigator.push(context, MaterialPageRoute(
  builder: (_) => ProductDetailPage(productId: 101, name: '商品A'),
));

// 目标页接收
class ProductDetailPage extends StatelessWidget {
  final int productId;
  final String name;

  const ProductDetailPage({super.key, required this.productId, required this.name});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(name)),
      body: Center(child: Text('ID: $productId')),
    );
  }
}

方式二:pop 返回数据

// 选择页面:选中后返回结果
class SelectCityPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: ['北京', '上海', '广州'].map((city) =>
        ListTile(
          title: Text(city),
          onTap: () => Navigator.of(context).pop(city), // 携带数据返回
        ),
      ).toList(),
    );
  }
}

// 调用方接收结果
Future<void> _selectCity() async {
  final city = await Navigator.push(
    context,
    MaterialPageRoute(builder: (_) => const SelectCityPage()),
  );
  if (city != null && mounted) {
    setState(() => _selectedCity = city);
  }
}

命名路由(集中管理)

适合中大型 App,类似 Vue Router 的 routes 配置:

// main.dart
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const HomePage(),
    '/login': (context) => const LoginPage(),
    '/profile': (context) => const ProfilePage(),
  },

  // 动态路由 + 未找到路由兜底
  onGenerateRoute: (settings) {
    // 解析动态路径,如 /product/123
    final uri = Uri.parse(settings.name ?? '');
    if (uri.pathSegments.first == 'product') {
      final id = int.tryParse(uri.pathSegments.last);
      if (id != null) {
        return MaterialPageRoute(
          builder: (_) => ProductDetailPage(productId: id, name: '商品'),
        );
      }
    }
    return MaterialPageRoute(builder: (_) => const NotFoundPage());
  },
)

// 跳转(类似 router.push('/login'))
Navigator.of(context).pushNamed('/login');
Navigator.of(context).pushReplacementNamed('/home');

// 带参数(arguments = query params)
Navigator.of(context).pushNamed(
  '/profile',
  arguments: {'userId': 123, 'name': 'Alice'},
);

// 目标页接收
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final userId = args['userId'];

自定义路由动画

// 淡入淡出
Navigator.of(context).push(PageRouteBuilder(
  transitionDuration: const Duration(milliseconds: 300),
  pageBuilder: (context, animation, _) => const NextPage(),
  transitionsBuilder: (context, animation, _, child) {
    return FadeTransition(opacity: animation, child: child);
  },
));

// 从右滑入(iOS 风格)
transitionsBuilder: (context, animation, _, child) {
  final tween = Tween(begin: const Offset(1.0, 0.0), end: Offset.zero)
      .chain(CurveTween(curve: Curves.easeInOut));
  return SlideTransition(position: animation.drive(tween), child: child);
},

底部导航栏 Tab 架构

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

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  // IndexedStack 保留各 Tab 的状态(类似 Vue keep-alive)
  final List<Widget> _pages = [
    const HomePage(),
    const SearchPage(),
    const CartPage(),
    const ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(index: _currentIndex, children: _pages),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        type: BottomNavigationBarType.fixed, // 4 项以上必须设置
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        onTap: (index) => setState(() => _currentIndex = index),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: '搜索'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: '购物车'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

IndexedStack vs PageView:

  • IndexedStack:所有 Tab 同时在树中,切换不重建(类似 keep-alive),内存占用稍高
  • PageView:懒加载,切换时重建(默认),支持手势左右滑动

WillPopScope(拦截返回键)

// 拦截 Android 物理返回键(或 iOS 侧滑返回)
WillPopScope(
  onWillPop: () async {
    // 返回 true = 允许返回;false = 阻止返回
    if (_hasUnsavedChanges) {
      final confirmed = await showDialog<bool>(
        context: context,
        builder: (_) => 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 confirmed ?? false;
    }
    return true;
  },
  child: Scaffold(...),
)