Flutter go_router 路由管理全面指南

2023-08-23  本文已影响0人  BlueSocks

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

前言

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

GoRouter 特性

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

添加插件

当前最新版本的 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(),
  ],
...
)

上一篇下一篇

猜你喜欢

热点阅读