首页关于文章作品集工具联系
返回文章列表

Flutter 路由与导航完全指南:从基础到企业级架构

路由(Routing)和导航(Navigation)是 APP 的骨架,决定了用户如何在不同页面之间穿梭。Flutter 提供了多种导航方式:从最简单的 Navigator.push 到声明式的 GoRouter,再到复杂的嵌套导航。这篇文章覆盖从小型项目到企业级应用的所有路由场景。

一、Flutter 导航的三种方式

  • 命令式导航(Imperative):Navigator.push/pop — 最直接,适合简单页面跳转
  • 命名式路由(Named Routes):通过路由名称 + 参数跳转,适合中大型项目
  • 声明式路由(Declarative / GoRouter):用 Widget 声明式定义路由,最适合大型项目

二、命令式导航基础

// ===== 最基本的页面跳转 =====
// 跳转并等待返回结果
final result = await Navigator.push(context,
  MaterialPageRoute(builder: (_) => const DetailPage(id: 42)),
);

// 返回上一页
Navigator.pop(context);

// 返回时携带数据
Navigator.pop(context, '这是返回的数据');

// 替换当前页面(不能返回)
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const LoginPage()));

// 清空栈并跳转(常用于登出)
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (_) => const HomePage()),
  (route) => route.isFirst,  // 只保留首页,移除其他所有页面
);

// 弹出新页面(底部半屏弹窗等)
showModalBottomSheet(
  context: context,
  builder: (context) => Container(height: 300, color: Colors.white, child: Text('底部弹窗')),
);

三、命名式路由(Named Routes)

命名路由将路由名称集中管理,实现路由解耦。

3.1 定义路由表

// lib/app_routes.dart

class AppRoutes {
  static const String home = '/';
  static const String login = '/login';
  static const String register = '/register';
  static const String profile = '/profile';
  static const String postDetail = '/post/:id';
  static const String settings = '/settings';
}

class AppRouter {
  static final Map<String, WidgetBuilder> routes = {
    AppRoutes.home: (context) => const HomePage(),
    AppRoutes.login: (context) => const LoginPage(),
    AppRoutes.register: (context) => const RegisterPage(),
    AppRoutes.profile: (context) => const ProfilePage(),
    AppRoutes.settings: (context) => const SettingsPage(),
  };

  // 动态路由(需要参数的路由)
  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    final uri = Uri.parse(settings.name!);
    
    if (uri.path == AppRoutes.postDetail) {
      final id = uri.queryParameters['id'] ?? '';
      return MaterialPageRoute(builder: (_) => PostDetailPage(postId: id));
    }
    // 未找到路由 → 返回 null 或 404 页面
    return null;
  }
}

3.2 在 MaterialApp 中注册

MaterialApp(
  title: 'MyApp',
  initialRoute: '/',
  routes: AppRouter.routes,
  onGenerateRoute: AppRouter.onGenerateRoute,
  // 全局未知路由拦截
  onUnknownRoute: (settings) => MaterialPageRoute(
    builder: (_) => const NotFoundPage(),
  ),
)

3.3 使用命名路由跳转

// 简单跳转
Navigator.pushNamed(context, AppRoutes.settings);

// 携带参数
Navigator.pushNamed(
  context,
  AppRoutes.postDetail,
  arguments: {'postId': '123', 'title': '文章标题'},
);

// 在目标页面获取参数
class PostDetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 方式一:ModalRoute 的 arguments
    final args = ModalRoute.of(context)?.settings.arguments as Map?;
    print(args?['postId']);
    
    // 方式二:onGenerateRoute 中解析的路径参数
    final routeArgs = ModalRoute.of(context);
    final postId = routeArgs?.settings.name?.split('/').last;
    
    return Scaffold(/* ... */);
  }
}

四、路由守卫(Auth Guard)

实际项目中,很多页面需要登录才能访问。路由守卫在跳转到目标页面前检查登录状态,未登录则重定向到登录页。

// lib/guards/auth_guard.dart
import 'package:flutter/foundation.dart';

/// 认证守卫:在跳转前检查登录状态
class AuthGuard {
  /// 需要认证的页面列表
  static const List<String> _protectedRoutes = [
    '/profile',
    '/settings',
    '/order',
  ];

  /// 检查是否已认证
  static Future<bool> isAuthenticated() async {
    final token = await StorageUtil.getToken();
    return token != null && token.isNotEmpty;
  }

  /// 守卫逻辑
  static Future<String?> check(RouteSettings settings) async {
    if (_protectedRoutes.any((r) => settings.name!.contains(r))) {
      if (!(await isAuthenticated())) {
        // 未登录 → 重定向到登录页,记录原始目标
        return '${AppRoutes.login}?redirect=${Uri.encodeComponent(settings.name!)}';
      }
    }
    return null;  // 已认证,放行
  }

  /// 在 MaterialApp 的 onGenerateRoute 中使用
  static Route<dynamic> guardNavigation(RouteSettings settings) async {
    final redirect = await check(settings);
    if (redirect != null) {
      return MaterialPageRoute(builder: (_) => LoginPage(redirectTo: redirect));
    }
    return null;
  }
}

五、深链接(Deep Link)

让外部链接可以直接打开 APP 内的指定页面。

// Android: android/app/src/main/AndroidManifest.xml
// <activity ...>
//   <intent-filter>
//     <action android:name="android.intent.action.VIEW"/>
//     <category android:name="android.intent.category.DEFAULT"/>
//     <category android:name="android.intent.category.BROWSABLE"/>
//     <data android:scheme="https" android:host="myapp.com"/>
//   </intent-filter>
// </activity>

// iOS: Runner/Info.plist
// CFBundleURLTypes
//   Item 0
//     CFBundleURLName = com.myapp.deeplink
//     CFBundleURLSchemes = (myapp)
//     CFBundleURLTypeIdentifier = myapp.deeplink
//     CFBundleURLRole = Editor

// Flutter 端处理
import 'package:uni_links/uni_links.dart';

Future<void> initDeepLinks() async {
  final appLinks = UniLinks();
  
  appLinks.linkStream.listen((Uri link) {
    // https://myapp.com/post/123
    switch (link.path) {
      case '/post':
        final postId = link.queryParameters['id'];
        navigatorKey.currentState?.pushNamed('/post/$postId');
        break;
      case '/user':
        navigatorKey.currentState?.pushNamed('/profile');
        break;
      default:
        // 打开首页
        navigatorKey.currentState?.pushNamed('/');
    }
  });
}

六、Nested Navigation(嵌套导航)

当 APP 有底部 Tab 导航,而每个 Tab 又有自己的子页面栈时,就需要嵌套导航。典型场景:Tab A 有 3 个子页面,Tab B 有 5 个子页面。

class MainTabPage extends StatefulWidget {
  @override
  _MainTabPageState createState() => _MainTabPageState();
}

class _MainTabPageState extends State<MainTabPage> with SingleTickerProviderStateMixin {
  late TabController _tabController;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedNavigator(
        key: const ValueKey('mainTab'),
        initialRoute: '/',
        observers: [_tabController],
        child: TabBarView(
          controller: _tabController,
          children: [
            // 每个 Tab 有自己独立的 Navigator 和页面栈
            Navigator(
              key: const ValueKey('homeNav'),
              initialRoute: '/',
              onGenerateRoute: (settings) {
                if (settings.name == '/') {
                  return MaterialPageRoute(builder: (_) => const HomeFeedPage());
                } else if (settings.name == '/detail') {
                  return MaterialPageRoute(builder: (_) => DetailPage());
                }
                return null;
              },
            ),
            Navigator(key: const ValueKey('searchNav'), initialRoute: '/', 
              onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => const SearchPage())),
            Navigator(key: const ValueKey('notifyNav'), initialRoute: '/', 
              onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => const NotifyPage())),
            Navigator(key: const ValueKey('mineNav'), initialRoute: '/', 
              onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => const MinePage())),
          ],
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _tabController.index,
        onTap: (i) {
          _tabController.animateTo(i);
          // 切换 Tab 时通知对应的 NestedNavigator
          NestedNavigators.of(context).key.currentState?.popUntil((r) => r.isFirst);
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: '发现'),
          BottomNavigationBarItem(icon: Icon(Icons.notifications), label: '消息'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

// 在 HomeFeedPage 中可以独立 push 子页面而不影响 Tab 切换
// 注意:必须使用 Navigator.of(context, rootNavigator: false) 来使用子 Navigator
Navigator.of(context, rootNavigator: false).pushNamed('/detail');

七、GoRouter — 企业级声明式路由

GoRouter 是社区最推荐的企业级路由方案,完全声明式定义,支持类型安全、深度链接、动画定制等功能。

// pubspec.yaml
dependencies:
  go_router: ^14.0.0

// lib/router/router_config.dart
part of './app_router.dart';

// 定义路由枚举(类型安全)
enum AppRoute { home, login, profile, postDetail, settings }

// GoRouter 配置
final goRouter = GoRouter(
  initialLocation: '/',
  debugLogDiagnostics: true,

  routes: [
    GoRoute(
      path: '/',
      name: AppRoute.home.name,
      builder: () => const HomePage(),
    ),
    GoRoute(
      path: '/login',
      name: AppRoute.login.name,
      builder: () => const LoginPage(),
    ),
    GoRoute(
      path: '/profile',
      name: AppRoute.profile.name,
      builder: () => const ProfilePage(),
      // 路由守卫
      redirect: (_, __) async {
        final isLoggedIn = await StorageUtil.isLoggedIn();
        return isLoggedIn ? null : const Redirect(AppRoute.login.name);
      },
    ),
    GoRoute(
      path: '/post/:id',
      name: AppRoute.postDetail.name,
      builder: () => const PostDetailPage(),
    ),
  ],

  // 错误页面
  errorBuilder: (error, stack) => ErrorScreen(error: error, stack: stack),
);

// 使用方式 —— 类型安全的导航
context.push(AppRoute.profile.name);           // 跳转到个人中心
context.pushNamed(AppRoute.postDetail.name, pathParams: {'id': '42'});  // 带参数
context.go('/post/42');  // 直接用路径也支持

// 带查询参数
context.go(Uri(path: '/search', queryParameters: {'q': 'flutter'}));

// Shell 路由(带底部导航的布局)
final shellRouter = GoRouter(
  routes: [],
  shellRoute: StatefulShellRoute.indexStack(
    builder: (shellContext, state, child) => MainShell(child: child),
    branches: [
      StatefulShellBranch(
        routes: [
          GoRoute(path: '/', page: () => HomeShellPage()),
          GoRoute(path: '/detail', page: () => DetailShellPage()),
        ],
      ),
    ],
  ),
);

八、路由最佳实践清单

  1. 统一管理路由名称:用常量或枚举集中定义,避免硬编码字符串
  2. 参数传递优先用构造函数:比 arguments 更类型安全
  3. 复杂对象传参用类实例:不要把大对象序列化为 JSON 字符串
  4. 路由守卫要处理循环重定向:避免 登录页 → 检查未登录 → 再到登录页 的死循环
  5. Nested Navigation 用 GlobalKey 区分:确保操作的是正确的 Navigator 栈
  6. 深链接要做合法性校验:防止恶意链接打开非法页面
  7. 过渡动画保持一致:自定义 PageTransitionsBuilder 统一风格
  8. Web 平台考虑 URL 同步:地址栏显示正确的路径

总结

Flutter 的导航体系非常灵活:个人项目用 Navigator.push/pop 就够用了;中型项目引入命名路由+路由守卫;大型项目上 GoRouter 声明式方案。关键不是选哪种框架,而是提前规划好路由结构,让整个 APP 的页面流转清晰可控。好的路由设计能让代码更易维护,用户体验也更流畅。