之前咱们介绍过Flutter 的一个路由办理器 fluro,本篇咱们介绍另一个十分受欢迎的路由办理第三方插件:go_router。本文翻译自 medium,为了便于阅览,做了些许个人理解后的修正,原文地址:medium.com/@antonio.ti…。

前语

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)来完结切换动作。完好代码:flutter-go_router-with-nested-tab-navigation。

路由护卫(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,
        );
      },
    );
  },
),

完好的示例代码能够在 GitHub 检查:自界说转场动画源码。

过错处理(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(),
  ],
...
)

完好的示例代码见 GitHub:
flutter-with-go_router: A simple app to show how to work with go_router。

我是岛上码农,微信大众号同名。如有问题能够加自己微信沟通,微信号:island-coder

:觉得有收获请点个赞鼓舞一下!

:收藏文章,方便回看哦!

:谈论沟通,互相进步!