Flutter中go

前言 go_router 是一个 Flutter 的第三方路由插件,相比 Flutter 自带的路由,go_router 更加灵活,而且简单易用。在 App 应

前言

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

  • 在 builder 参数中, 我们返回导航用的壳,一个简单的带有BottomNavigationBarScaffold,这里需要记得将navigationShell传给页面,因为我们需要用它来导航到其他分支,例如从Home到 Shope。
  • 在路由分支数组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

到此这篇关于Flutter中go_router路由管理的使用指南的文章就介绍到这了,更多相关Flutter go_router内容请搜索好代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持好代码网!

您可能有感兴趣的文章
Flutter入门学习Dart语言变量及基本如何使用概念

Flutter 异步编程之单线程下异步模型图文示例详解

如何如何使用Flutter开发一款电影APP详解

Flutter进阶之如何实现动画效果(二)

Flutter定义tabbar底部导航路由跳转的方法