Flutter 从入门到精通:深入 Navigator 2.0 - GoRouter 路由完全指南

在上一篇关于导航的文章中,我们学习了 Navigator 1.0,也就是大家所熟知的 pushpop 方法。对于简单的线性页面流,它工作得非常好。但随着应用变得越来越复杂,Navigator 1.0 的局限性也逐渐显现。

想象以下场景:

  • 深度链接 (Deep Linking) :用户点击一个 URL (myapp://details/123),需要直接打开应用的特定页面。
  • Web 应用支持 :在 Flutter Web 中,你需要同步浏览器的地址栏和历史记录(前进/后退按钮)。
  • 复杂嵌套导航:应用拥有底部导航栏,每个标签页都维护着自己的导航堆栈。

Navigator 1.0 (命令式路由) 来处理这些需求,会变得异常棘手和混乱。

为了解决这些问题,Flutter 团队引入了 Navigator 2.0 (声明式路由)。但不得不承认,Navigator 2.0 的原生 API(如 Router, RouterDelegate, RouteInformationParser)非常复杂,学习曲线陡峭,劝退了大量开发者。

幸运的是,我们不必直接和这些复杂的 API 打交道。Flutter 官方团队推出了 GoRouter,一个构建在 Navigator 2.0 之上的、强大且简单易用的路由库。它完美地简化了声明式路由,并成为当前 Flutter 路由管理的首选方案。

这篇文章,就是你掌握 GoRouter 的完全指南。

1. 为什么选择 GoRouter?

  • 基于 URL :以我们熟悉的 URL 字符串来定义和导航页面,直观且易于管理。
  • 声明式 :你只需要定义好路由表,GoRouter 会自动处理底层的 Navigator 2.0 逻辑。
  • 完美支持 Web:自动处理浏览器地址栏同步和历史记录。
  • 类型安全:新版本支持类型安全地传递参数,减少运行时错误。
  • 轻松处理复杂场景:无论是参数传递、嵌套路由、还是路由重定向(如登录拦截),都提供了优雅的解决方案。

2. GoRouter 实战演练

让我们从一个简单的项目开始,一步步探索 GoRouter 的核心功能。

第一步:添加依赖

首先,在你的 pubspec.yaml 文件中添加 go_router 依赖。

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.1.0 # 建议使用最新版本

然后运行 flutter pub get 安装。

第二步:创建基础路由配置

创建一个单独的文件来管理路由,例如 lib/router/app_router.dart

dart 复制代码
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';

// 导入你的页面 Widget
import '../screens/home_screen.dart';
import '../screens/details_screen.dart';
import '../screens/not_found_screen.dart';

final GoRouter router = GoRouter(
  // 404 页面
  errorBuilder: (context, state) => const NotFoundScreen(),

  // 路由配置
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/details',
      builder: (context, state) => const DetailsScreen(),
    ),
  ],
);
  • GoRouter(...):这是路由配置的入口。
  • errorBuilder:当用户访问一个不存在的路径时,会显示这个页面。
  • routes: 一个 GoRoute 列表,定义了应用的每个页面。
    • path: 页面的访问路径 (URL)。
    • builder: 一个函数,返回对应路径的 Widget
第三步:集成到 MaterialApp

现在,回到你的 main.dart,使用 MaterialApp.router 来集成 GoRouter

dart 复制代码
import 'package:flutter/material.dart';
import 'router/app_router.dart'; // 导入你的路由配置

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // 使用 MaterialApp.router 构造函数
    return MaterialApp.router(
      title: 'GoRouter Demo',
      // 将我们创建的 router 对象配置给 routerConfig
      routerConfig: router,
    );
  }
}

至此,你的应用已经由 GoRouter 全权接管路由了!

第四步:页面跳转 (Navigation)

GoRouter 提供了一组 BuildContext 的扩展方法来进行导航,非常方便。

在你的 HomeScreen 中:

dart 复制代码
// home_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home Screen')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 跳转到 /details 页面
            context.go('/details');
          },
          child: const Text('Go to Details'),
        ),
      ),
    );
  }
}
  • context.go(path):跳转到新页面,它会替换 当前的路由堆栈。这更适用于 Web 行为或主标签页切换。
  • context.push(path):将新页面推入 到当前的路由堆栈之上,这和 Navigator.push 行为一致,通常用于从列表页进入详情页。

在详情页,我们可以使用 context.pop() 返回:

dart 复制代码
// details_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class DetailsScreen extends StatelessWidget {
  const DetailsScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details Screen')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 返回上一页
            context.pop();
          },
          child: const Text('Go Back'),
        ),
      ),
    );
  }
}
第五步:传递参数

这是 GoRouter 的强项。我们有三种常见的传参方式:

1. 路径参数 (Path Parameters)

这适用于传递简单的 ID 或标识符。

修改路由配置,在 path 中使用冒号 (:) 定义参数:

dart 复制代码
// app_router.dart
GoRoute(
  // :id 是一个占位符,匹配 /details/任意字符串
  path: '/details/:id', 
  builder: (context, state) {
    // 通过 state.pathParameters 获取路径参数
    final String id = state.pathParameters['id'] ?? '0';
    return DetailsScreen(id: id);
  },
),

修改 DetailsScreen 以接收参数:

dart 复制代码
// details_screen.dart
class DetailsScreen extends StatelessWidget {
  final String id;
  const DetailsScreen({super.key, required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details Screen')),
      body: Center(
        child: Text('You are viewing details for item: $id'),
      ),
      // ...
    );
  }
}

跳转时,直接在路径中提供参数值:

dart 复制代码
// home_screen.dart
context.push('/details/123'); // 跳转到详情页,并传递 id '123'

2. 查询参数 (Query Parameters)

适用于传递过滤、搜索等非必需参数。

路由配置不需要改变。跳转时,像写 URL 一样附加参数:

dart 复制代码
context.push('/details/123?source=homepage&sort=asc');

DetailsScreen 中,通过 state.uri.queryParameters 获取:

dart 复制代码
// details_screen.dart builder function
builder: (context, state) {
  final String id = state.pathParameters['id'] ?? '0';
  final String? source = state.uri.queryParameters['source']; // "homepage"
  return DetailsScreen(id: id, source: source);
}

3. extra 参数

当你想传递一个复杂的自定义对象(如 Model 实例)时,extra 参数是最好的选择。

跳转时,在 pushgo 方法中传入 extra

dart 复制代码
// 假设你有一个 Product 类
final product = Product(id: '123', name: 'Flutter Book');
context.push('/details/123', extra: product);

DetailsScreenbuilder 中接收它:

dart 复制代码
builder: (context, state) {
  final String id = state.pathParameters['id']!;
  // state.extra 是 Object? 类型,需要进行类型转换
  final Product? product = state.extra as Product?; 
  return DetailsScreen(id: id, product: product);
}

GoRouter 让实现带底部导航栏的复杂布局变得非常简单。我们将使用 StatefulShellRoute.indexedStack

想象一个应用有三个主页面:Home, Feed, Settings

dart 复制代码
// app_router.dart
// 1. 定义一个 shell widget,它包含 BottomNavigationBar 和用于显示子页面的 body
import '../screens/scaffold_with_nav_bar.dart'; 

final GoRouter router = GoRouter(
  initialLocation: '/home', // 初始页面
  routes: [
    // 2. 使用 StatefulShellRoute.indexedStack 定义嵌套路由
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        // 返回你的 Shell UI (例如,一个带 BottomNavigationBar的 Scaffold)
        return ScaffoldWithNavBar(navigationShell: navigationShell);
      },
      branches: [
        // 第一个分支 (tab)
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              builder: (context, state) => const HomeScreen(),
            ),
          ],
        ),
        // 第二个分支
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/feed',
              builder: (context, state) => const FeedScreen(),
              routes: [
                // Feed 页面下的子页面
                GoRoute(
                  path: 'details/:id', // 路径会自动拼接为 /feed/details/:id
                  builder: (context, state) => DetailsScreen(id: state.pathParameters['id']!),
                ),
              ]
            ),
          ],
        ),
        // 第三个分支
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/settings',
              builder: (context, state) => const SettingsScreen(),
            ),
          ],
        ),
      ],
    ),
  ],
);

你的 ScaffoldWithNavBar 会接收一个 navigationShell 对象,用它来构建 BottomNavigationBar 并显示当前分支的页面。 StatefulShellRoute 的美妙之处在于,当你切换标签页时,每个分支的导航状态都会被完整保留!

第七步:重定向 (Redirect) - 登录验证

GoRouterredirect 功能是实现路由守卫(例如登录拦截)的利器。

dart 复制代码
// app_router.dart
final GoRouter router = GoRouter(
  redirect: (BuildContext context, GoRouterState state) {
    // 模拟一个登录状态
    final bool loggedIn = false; // 在真实应用中,你会从状态管理中获取
    
    // 检查用户是否在访问登录页
    final bool isLoggingIn = state.matchedLocation == '/login';

    // 如果用户未登录且不在登录页,重定向到登录页
    if (!loggedIn && !isLoggingIn) {
      return '/login';
    }
    
    // 如果用户已登录且在访问登录页,重定向到首页
    if (loggedIn && isLoggingIn) {
      return '/';
    }

    // 其他情况,不重定向
    return null;
  },
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/profile',
      builder: (context, state) => const ProfileScreen(),
    ),
  ],
);

这个 redirect 函数会在每次导航发生前被调用。你可以根据应用状态(如登录状态、用户权限等)返回一个新的路径来中断并重定向导航,或者返回 null 表示允许本次导航。

总结

Navigator 2.0 的声明式思想为 Flutter 带来了更强大、更灵活的路由系统,而 GoRouter 则是我们驾驭这套系统的最佳工具。

通过本教程,我们学习了 GoRouter 的核心用法:

  1. 基础配置 :通过 GoRouterGoRoute 定义路由表。
  2. 页面导航 :使用 context.go, context.push, context.pop
  3. 参数传递 :掌握了路径参数、查询参数和 extra 对象传参。
  4. 嵌套路由 :使用 StatefulShellRoute 轻松实现复杂的 UI 布局。
  5. 路由重定向 :通过 redirect 实现登录拦截等路由守卫。

掌握 GoRouter,你将能够自信地构建任何复杂的 Flutter 应用,并轻松应对 Web 和深度链接带来的挑战。

相关推荐
lichong9512 小时前
【macOS 版】Android studio jdk 1.8 gradle 一键打包成 release 包的脚本
android·java·前端·macos·android studio·大前端·大前端++
Kapaseker3 小时前
Kotlin 跨平台开发中的权衡
android·ios·kotlin
恋猫de小郭3 小时前
来了解一下,为什么你的 Flutter WebView 在 iOS 26 上有点击问题?
android·前端·flutter
newchenxf3 小时前
AndroidStudio版本和AGP版本和gradle版本以及kotlin gradle plugin版本关系梳理 2025
android·开发语言·kotlin
CocoaKier4 小时前
苹果上线App Store Web版本,以后浏览外区更方便了
ios·apple
曹绍华5 小时前
kotlin扩展函数是如何实现的
android·开发语言·kotlin
LSL666_10 小时前
5 Repository 层接口
android·运维·elasticsearch·jenkins·repository
alexhilton14 小时前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
2501_9400940216 小时前
emu系列模拟器最新汉化版 安卓版 怀旧游戏模拟器全集附可运行游戏ROM
android·游戏·安卓·模拟器