美文网首页
Flutter go_router 路由管理全面指南

Flutter go_router 路由管理全面指南

作者: BlueSocks | 来源:发表于2023-08-23 17:16 被阅读0次

    本篇我们介绍一个非常受欢迎的路由管理第三方插件:go_router。

    前言

    go_router 是一个 Flutter 的第三方路由插件,相比 Flutter 自带的路由,go_router 更加灵活,而且简单易用。在 App 应用中,如果你想自己控制路由的定义和管理方式,那么它将十分有用。同时,对于 Web 应用来说,go_router 也提供了很好的支持。 使用 go_router 后,你可以定义 URL 的格式,使用 URL 跳转,处理深度链接以及其他一系列的导航相关的应用场景。

    GoRouter 特性

    GoRouter 针对页面导航提供了下面这些特性:

    • 使用模板语法解析路由路径和路由查询(query)参数;
    • 支持单个目标路由展示多个页面(子路由);
    • 重定向:可以基于应用状态跳转到不同的URL,比如用户没有登录时跳转到登录页;
    • 使用 StatefulShellRoute 可以支持嵌套的 Tab 导航;
    • 同时支持 Material 风格和 Cupertino 风格应用;
    • 兼容 Navigator API 。

    添加插件

    当前最新版本的 go_router 是10.0.0(6.3.0版本以上需要 Dart 2.18),可以根据自己的需要添加相应的版本。在 pubspec.yaml 中加入依赖的版本即可,下面是以7.1.1版本为例。

    dependencies:
      go_router: ^7.1.1
    
    

    路由配置

    引入 go_router 插件后,就可以在应用中配置 GoRouter,代码如下:

    import 'package:go_router/go_router.dart';
    
    // GoRouter configuration
    final _router = GoRouter(
      initialLocation: '/',
      routes: [
        GoRoute(
          name: 'home', // Optional, add name to your routes. Allows you navigate by name instead of path
          path: '/',
          builder: (context, state) => HomeScreen(),
        ),
        GoRoute(
          name: 'page2',
          path: '/page2',
          builder: (context, state) => Page2Screen(),
        ),
      ],
    );
    
    

    然后,我们就可以通过MaterialApp.routerCupertinoApp.router构造函数来使用 GoRouter,并且将 routerConfig 参数设置为我们前面定义的 GoRouter 配置对象。

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

    接下来就可以愉快地玩耍 GoRouter 了。

    路由参数

    GoRouter 的每一个路由都通过 GoRoute对象来配置,我们可以在构建 GoRoute 对象时来配置路由参数。路由参数典型的就是路径参数,比如 /path/:{路径参数},这个时候 GoRoute的路径参数和很多 Web 框架的路由是一样的,通过一个英文冒号加参数名称就可以配置,之后我们可以在回调方法中通过 GoRouterState 对象获取路径参数,这个参数就可以传递到路由跳转目的页面。

    GoRoute(
      path: '/fruits/:id',
      builder: (context, state) {
         final id = state.params['id'] // Get "id" param from URL
         return FruitsPage(id: id);
      },
    ),
    
    

    同样,也可以从GoRouterState中获取 URL 路径中的查询(query)参数,例如下面的代码就是从/fruits?search=antonio中获取search参数。

    GoRoute(
      path: '/fruits',
      builder: (context, state) {
        final search = state.queryParams['search'];
        return FruitsPage(search: search);
      },
    ),
    
    

    添加子路由

    路由匹配后可以支持多个页面(即子路由),当一个新的页面在旧的页面之上展示时,这个时候的效果和调用 push方法是一样的,。如果页面提供了 AppBar 组件的话,那么会自动增加返回按钮。 要使用子路由,我们只需要在上级路由中增加对应的下级路由即可,代码如下。

    GoRoute(
      path: '/fruits',
      builder: (context, state) {
        return FruitsPage();
      },
      routes: <RouteBase>[ // Add child routes
        GoRoute(
          path: 'fruits-details', // NOTE: Don't need to specify "/" character for router’s parents
          builder: (context, state) {
            return FruitDetailsPage();
          },
        ),
      ],
    )
    
    

    页面导航

    GoRouter 提供了多种方式跳转到目的页面,比如使用context.go()跳转到指定的 URL 地址。

    build(BuildContext context) {
      return TextButton(
        onPressed: () => context.go('/fruits/fruit-detail'),
      );
    }
    
    

    也可以使用路由的名称进行跳转,这个时候调用context.goNamed()即可。

    build(BuildContext context) {
      return TextButton(
        // remember to add "name" to your routes
        onPressed: () => context.goNamed('fruit-detail'),
      );
    }
    
    

    如果要构建查询参数,那么可以使用 Uri 类来构建路由路径。

    context.go(
      Uri(
        path: '/fruit-detail',
        queryParameters: {'id': '10'},
       ).toString(),
    );
    
    

    如果要从当前页面返回的话,调用context.pop()即可。

    嵌套导航

    有些应用在同一个页面展示多个子页面,例如 BottomNavigationBar 在进行导航的时候就可以一直保留在屏幕底部。这种其实是嵌套导航,在 GoRouter 里,可以通过StatefulShellRoute来实现。 StatefulShellRoute不直接使用根导航(root Navigator),而是通过不同的导航的子路由来实现嵌套导航。对于每一个导航分支,都会创建各自独立的导航,相当于是一个并行导航树,从而实现了有状态的嵌套导航。 当我们使用BottomNavigationBar的时候,就可以为很方便地为每一个 Tab 配置一个持久的导航状态。 StatefulShellRoute通过指定一个StatefulShellBranch类型的列表来完成,列表每一个元素代表路由树的一个独立的有状态的分支。StatefulShellBranch为每个分支提供了根路由和 Navigator key(GlobalKey),并提供了可选的初始默认路由地址。 我们来看看具体怎么实现。 首先创建我们的 GoRouter 对象,这个时候我们需要添加StatefulShellRoute.indexedStack到路由中,这个类负责创建嵌套路由。StatefulShellRoute.indexedStack() 实际上是使用了 IndexedStack创建了一个StatefulShellRoute。 这个构造函数使用IndexedStack来管理每个分支导航的页面,示例代码如下:

    // Create keys for `root` & `section` navigator avoiding unnecessary rebuilds
    final _rootNavigatorKey = GlobalKey<NavigatorState>();
    final _sectionNavigatorKey = GlobalKey<NavigatorState>();
    
    final router = GoRouter(
      navigatorKey: _rootNavigatorKey,
      initialLocation: '/feed',
      routes: <RouteBase>[
        StatefulShellRoute.indexedStack(
          builder: (context, state, navigationShell) {
            // Return the widget that implements the custom shell (e.g a BottomNavigationBar).
            // The [StatefulNavigationShell] is passed to be able to navigate to other branches in a stateful way.
            return ScaffoldWithNavbar(navigationShell);
          },
          branches: [
            // The route branch for the 1º Tab
            StatefulShellBranch(
              navigatorKey: _sectionNavigatorKey,
              // Add this branch routes
              // each routes with its sub routes if available e.g feed/uuid/details
              routes: <RouteBase>[
                GoRoute(
                  path: '/feed',
                  builder: (context, state) => const FeedPage(),
                  routes: <RouteBase>[
                    GoRoute(
                      path: 'detail',
                      builder: (context, state) => const FeedDetailsPage(),
                    )
                  ],
                ),
              ],
            ),
    
            // The route branch for 2º Tab
            StatefulShellBranch(routes: <RouteBase>[
              // Add this branch routes
              // each routes with its sub routes if available e.g shope/uuid/details
              GoRoute(
                path: '/shope',
                builder: (context, state) => const ShopePage(),
              ),
            ])
          ],
        ),
      ],
    );
    
    

    在上面的代码中,我们在路由中加入了StatefulShellRoute.indexedStack(),由它负责创建路由分支以及返回一个自定义的导航壳,这里是BottomNavigationBar

    1. 在 builder 参数中, 我们返回导航用的壳,一个简单的带有BottomNavigationBarScaffold,这里需要记得将navigationShell传给页面,因为我们需要用它来导航到其他分支,例如从Home到 Shope。
    2. 在路由分支数组branches中,我们提供了一个StatefulShellBranch 的数组。这里只需要给第一个元素的navigatorKey提供之前创建的全局的_sectionNavigatorKey。其他分支则使用默认的 key。同时,为每个分支提供了一个RouteBase列表,该列表是对应分支的路由。

    下面是我们定义的带有BottomNavigationBar的自定义导航壳的代码。

    import 'package:flutter/material.dart';
    import 'package:go_router/go_router.dart';
    
    class ScaffoldWithNavbar extends StatelessWidget {
      const ScaffoldWithNavbar(this.navigationShell, {super.key});
    
      /// The navigation shell and container for the branch Navigators.
      final StatefulNavigationShell navigationShell;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: navigationShell,
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: navigationShell.currentIndex,
            items: const [
              BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
              BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shope'),
            ],
            onTap: _onTap,
          ),
        );
      }
    
      void _onTap(index) {
        navigationShell.goBranch(
          index,
          // A common pattern when using bottom navigation bars is to support
          // navigating to the initial location when tapping the item that is
          // already active. This example demonstrates how to support this behavior,
          // using the initialLocation parameter of goBranch.
          initialLocation: index == navigationShell.currentIndex,
        );
      }
    }
    
    

    在上面的代码中,实际上就是构建带有BottomNavigationBarScaffold然后 body 是从路由里获取的navigationShell. 路由分支及页面的切换通过_onTap(index) 实现,当点击某个 Tab 时,就使用navigationShell.goBranch(index)来完成切换动作。

    路由守卫(Guards)

    有些路由地址需要守卫,例如对于没有登录的用户,有些页面就无法访问。GoRouter 可以设置全局的重定向路由。最常见的一个场景就是对于没有登录的用户,跳转到/login 登录页面。 在 GoRouter 中,可以通过redirect 参数配置重定向,这是一个GoRouterRedirect的回调方法。如果要基于应用状态更改跳转都只,那么既可以在GoRouterGoRoute的构造方法中增加redirect参数。其中 GoRoute是只针对当前路由进行跳转处理,而GoRouter这是全局处理。下面是示例代码,如果不需要跳转,则在回调方法中返回 null即可。

    GoRouter(
      redirect: (BuildContext context, GoRouterState state) {
        final isAuthenticated = // your logic to check if user is authenticated
        if (!isAuthenticated) {
          return '/login';
        } else {
          return null; // return "null" to display the intended route without redirecting
         }
       },
      ...
    
    

    也可以指定一个redirectLimit参数来限制最大的跳转次数,这个值默认是5。如果超过了跳转次数,则会显示一个错误页面。

    转场动画

    GoRouter支持为每个 GoRoute自定义转场动画,这可以通过GoRoute的构造函数的pageBuilder 参数来完成,下面是示例代码。

    GoRoute(
      path: '/fruit-details',
      pageBuilder: (context, state) {
        return CustomTransitionPage(
          key: state.pageKey,
          child: FruitDetailsScreen(),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            // Change the opacity of the screen using a Curve based on the the animation's value
            return FadeTransition(
              opacity: CurveTween(curve: Curves.easeInOutCirc).animate(animation),
              child: child,
            );
          },
        );
      },
    ),
    
    

    错误处理(404页面)

    go_routerMaterialAppCupertinoApp定义了默认的错误页面,也可以通过 errorBuilder 参数自定义错误页面,代码如下。

    GoRouter(
      /* ... */
      errorBuilder: (context, state) => ErrorPage(state.error),
    );
    
    

    类型安全路由

    除了使用 URL 进行路由导航外,go_router 也通过go_router_builder插件提供了类型安全路由,这可以通过代码生成来完成。要使用这种方式,需要在pubspec.yaml增加下面这些依赖。

    dev_dependencies:
      go_router_builder: ^1.0.16
      build_runner: ^2.3.3
      build_verify: ^3.1.0
    
    

    定义路由

    Then define each route as a class extending GoRouteData and overriding the build method.

    class HomeRoute extends GoRouteData {
      const HomeRoute();
      
      @override
      Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
    }
    
    

    路由树

    路由树基于每个顶层的路由来定义,代码如下。

    import 'package:go_router/go_router.dart';
    
    part 'go_router.g.dart'; // name of generated file
    
    // Define how your route tree (path and sub-routes)
    @TypedGoRoute<HomeScreenRoute>(
        path: '/home',
        routes: [ // Add sub-routes
          TypedGoRoute<SongRoute>(
            path: 'song/:id',
          )
        ]
    )
    
    // Create your route screen that extends "GoRouteData" and @override "build"
    // method that return the screen for this route
    @immutable
    class HomeScreenRoute extends GoRouteData {
      @override
      Widget build(BuildContext context) {
        return const HomeScreen();
      }
    }
    
    @immutable
    class SongRoute extends GoRouteData {
      final int id;
      const SongRoute({required this.id});
    
      @override
      Widget build(BuildContext context) {
        return SongScreen(songId: id.toString());
      }
    }
    
    

    之后可以运行代码生成来构建类型安全路由。

    flutter pub global activate build_runner // Optional, if you already have build_runner activated so you can skip this step
    flutter pub run build_runner build
    
    

    导航的时候,就不再需要使用 URL的方式了,可以构建一个GoRouteData对象然后调用go()即可。

    TextButton(
      onPressed: () {
        const SongRoute(id: 2).go(context);
      },
      child: const Text('Go to song 2'),
    ),
    
    

    路由跳转监测

    go_router还提供了一个非常有用的特性,那就是路由导航监测NavigatorObserver。可以通过给GoRouter增加一个NavigatorObserver对象来监听路由行为,例如 pushpop 或路由替换(replace)。这可以通过自定义 NavigatorObserver 的子类完成。

    class MyNavigatorObserver extends NavigatorObserver {
      @override
      void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
        log('did push route');
      }
    
      @override
      void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
        log('did pop route');
      }
    }
    
    

    之后,在GoRouterobservers 参数中增加自定义的MyNavigatorObserver 即可完成对所有触发路由跳转的行为的监听。

    GoRouter(
      ...
      observers: [ // Add your navigator observers
        MyNavigatorObserver(),
      ],
    ...
    )
    
    

    相关文章

      网友评论

          本文标题:Flutter go_router 路由管理全面指南

          本文链接:https://www.haomeiwen.com/subject/xcwpmdtx.html