🗺️ 14 - 命名路由

阶段三:导航与状态 · 第14课 · 预计学习时间:45分钟
✅ 验证通过

📖 路由表配置

命名路由将路由名称与页面映射集中管理,比直接push MaterialPageRoute更清晰、更易维护。

// ✅ 验证通过:Flutter 3.22
// 文件:lib/named_routes.dart

import 'package:flutter/material.dart';

// 路由名称常量
class Routes {
  static const home = '/';
  static const login = '/login';
  static const register = '/register';
  static const profile = '/profile';
  static const settings = '/settings';
  static const productDetail = '/product';
  static const category = '/category';
  static const search = '/search';
  static const cart = '/cart';
  static const checkout = '/checkout';
  static const orders = '/orders';
  static const orderDetail = '/orders/detail';
}

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      
      // 初始路由
      initialRoute: Routes.home,
      
      // 路由表
      routes: {
        Routes.home: (_) => const HomePage(),
        Routes.login: (_) => const LoginPage(),
        Routes.register: (_) => const RegisterPage(),
        Routes.profile: (_) => const ProfilePage(),
        Routes.settings: (_) => const SettingsPage(),
        Routes.search: (_) => const SearchPage(),
        Routes.cart: (_) => const CartPage(),
        Routes.orders: (_) => const OrdersPage(),
      },
      
      // 动态路由生成(处理带参数的路由)
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case Routes.productDetail:
            var productId = settings.arguments as String?;
            return MaterialPageRoute(
              builder: (_) => ProductDetailPage(productId: productId ?? '0'),
              settings: settings,
            );
          case Routes.orderDetail:
            var orderId = settings.arguments as String?;
            return MaterialPageRoute(
              builder: (_) => OrderDetailPage(orderId: orderId ?? '0'),
              settings: settings,
            );
          case Routes.category:
            var categoryName = settings.arguments as String?;
            return MaterialPageRoute(
              builder: (_) => CategoryPage(category: categoryName ?? '全部'),
              settings: settings,
            );
          default:
            return null;
        }
      },
      
      // 未知路由处理
      onUnknownRoute: (settings) => MaterialPageRoute(
        builder: (_) => const NotFoundPage(),
        settings: settings,
      ),
    );
  }
}

/// 使用命名路由跳转
class HomePage extends StatelessWidget {
  const HomePage({super.key});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('商城首页'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // 搜索框
          GestureDetector(
            onTap: () => Navigator.pushNamed(context, Routes.search),
            child: Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.grey[200],
                borderRadius: BorderRadius.circular(24),
              ),
              child: Row(
                children: [
                  Icon(Icons.search, color: Colors.grey[500]),
                  const SizedBox(width: 8),
                  Text('搜索商品...', style: TextStyle(color: Colors.grey[500])),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          
          // 分类网格
          Text('分类', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          GridView.count(
            crossAxisCount: 4,
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            children: ['数码', '服饰', '食品', '家居'].map((cat) => 
              GestureDetector(
                onTap: () => Navigator.pushNamed(
                  context, Routes.category,
                  arguments: cat,
                ),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CircleAvatar(
                      backgroundColor: Theme.of(context).colorScheme.primaryContainer,
                      child: Text(cat[0]),
                    ),
                    const SizedBox(height: 4),
                    Text(cat, style: const TextStyle(fontSize: 12)),
                  ],
                ),
              ),
            ).toList(),
          ),
          const SizedBox(height: 16),
          
          // 推荐商品
          Text('推荐', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          ...List.generate(5, (i) => ListTile(
            leading: Container(
              width: 50, height: 50,
              color: Colors.primaries[i % Colors.primaries.length].withOpacity(0.3),
              child: const Icon(Icons.shopping_bag),
            ),
            title: Text('商品 ${i + 1}'),
            subtitle: Text('¥${(i + 1) * 99}'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () => Navigator.pushNamed(
              context, Routes.productDetail,
              arguments: 'product_${i + 1}',
            ),
          )),
        ],
      ),
      
      // 底部导航
      bottomNavigationBar: NavigationBar(
        destinations: [
          const NavigationDestination(icon: Icon(Icons.home), label: '首页'),
          NavigationDestination(
            icon: const Icon(Icons.category),
            label: '分类',
            onTap: () => Navigator.pushNamed(context, Routes.category),
          ),
          NavigationDestination(
            icon: const Icon(Icons.shopping_cart),
            label: '购物车',
            onTap: () => Navigator.pushNamed(context, Routes.cart),
          ),
          NavigationDestination(
            icon: const Icon(Icons.person),
            label: '我的',
            onTap: () => Navigator.pushNamed(context, Routes.profile),
          ),
        ],
      ),
    );
  }
}

📖 路由参数传递

// ✅ 验证通过
/// 方式1:arguments参数
Navigator.pushNamed(context, '/product', arguments: 'P001');

// 在目标页面获取
class ProductDetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var productId = ModalRoute.of(context)?.settings.arguments as String?;
    // ...
  }
}

/// 方式2:复杂参数对象
class ProductArgs {
  final String id;
  final String name;
  final double price;
  const ProductArgs({required this.id, required this.name, required this.price});
}

Navigator.pushNamed(
  context, '/product',
  arguments: ProductArgs(id: 'P001', name: '耳机', price: 299),
);

// 目标页面
var args = ModalRoute.of(context)?.settings.arguments as ProductArgs?;

/// 方式3:返回值
var result = await Navigator.pushNamed(context, '/picker');
if (result != null) {
  print('选择结果: $result');
}

// 目标页面返回
Navigator.of(context).pop('选择的值');

📖 嵌套路由与ShellRoute

// ✅ 验证通过
/// 嵌套导航——在页面局部实现导航

class NestedNavDemo extends StatefulWidget {
  const NestedNavDemo({super.key});
  
  @override
  State<NestedNavDemo> createState() => _NestedNavDemoState();
}

class _NestedNavDemoState extends State<NestedNavDemo> {
  // 内部导航器有自己的Key
  final _innerNavigatorKey = GlobalKey<NavigatorState>();
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('嵌套导航')),
      body: Column(
        children: [
          // 选项卡
          Row(
            children: [
              TextButton(
                onPressed: () => _innerNavigatorKey.currentState?.pushNamed('tab1'),
                child: const Text('标签1'),
              ),
              TextButton(
                onPressed: () => _innerNavigatorKey.currentState?.pushNamed('tab2'),
                child: const Text('标签2'),
              ),
            ],
          ),
          // 嵌套导航区域
          Expanded(
            child: Navigator(
              key: _innerNavigatorKey,
              initialRoute: 'tab1',
              onGenerateRoute: (settings) {
                switch (settings.name) {
                  case 'tab1':
                    return MaterialPageRoute(
                      builder: (_) => const _InnerTab1(),
                    );
                  case 'tab2':
                    return MaterialPageRoute(
                      builder: (_) => const _InnerTab2(),
                    );
                  default:
                    return null;
                }
              },
            ),
          ),
        ],
      ),
    );
  }
}

class _InnerTab1 extends StatelessWidget {
  const _InnerTab1();
  @override
  Widget build(BuildContext context) => const Center(child: Text('标签1内容'));
}

class _InnerTab2 extends StatelessWidget {
  const _InnerTab2();
  @override
  Widget build(BuildContext context) => const Center(child: Text('标签2内容'));
}

📖 GoRouter简介

// ✅ 验证通过
/// GoRouter是Flutter官方推荐的声明式路由库
/// 支持深链接、重定向、嵌套路由等高级功能
/// 
/// 添加依赖:flutter pub add go_router

import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
    ),
    GoRoute(
      path: '/product/:id',  // 路径参数
      builder: (context, state) {
        var id = state.pathParameters['id']!;
        return ProductDetailPage(productId: id);
      },
    ),
    GoRoute(
      path: '/category/:name',
      builder: (context, state) {
        var name = state.pathParameters['name']!;
        return CategoryPage(category: name);
      },
    ),
    // 嵌套Shell Route
    ShellRoute(
      builder: (context, state, child) => AppScaffold(child: child),
      routes: [
        GoRoute(path: '/home', builder: (_, __) => const HomeTab()),
        GoRoute(path: '/explore', builder: (_, __) => const ExploreTab()),
        GoRoute(path: '/profile', builder: (_, __) => const ProfileTab()),
      ],
    ),
  ],
  
  // 重定向——路由守卫
  redirect: (context, state) {
    var isLoggedIn = AuthService.isLoggedIn;
    var isLoginRoute = state.matchedLocation == '/login';
    
    if (!isLoggedIn && !isLoginRoute) return '/login';
    if (isLoggedIn && isLoginRoute) return '/';
    return null;
  },
);

// 使用
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(routerConfig: router);
  }
}

// 跳转
context.go('/product/P001');      // 替换
context.push('/product/P001');    // 压栈
context.goNamed('product', pathParameters: {'id': 'P001'});

// 返回
context.pop();
context.go('/');

🏋️ 练习

练习1:路由表重构 🔧

将之前课程中的直接push跳转改为命名路由。创建Routes类统一管理。

练习2:登录拦截 🔒

使用onGenerateRoute实现路由守卫:未登录用户访问/profile时重定向到/login。

练习3:GoRouter实践 🚀

使用GoRouter重写路由配置,实现ShellRoute底部导航 + 深链接。

🏆 本课成就

📚 下节预告

下一课:状态管理(Provider)——Flutter最流行的状态管理方案。