Usage

Dependencies

To use go_router_builder, you need to have the following dependencies in pubspec.yaml.

dependencies:
  # ...along with your other dependencies
  go_router: ^16.2.0

dev_dependencies:
  # ...along with your other dev-dependencies
  build_runner: ^2.6.0
  go_router_builder: ^4.0.1

Source code

Instructions below explain how to create and annotate types to use this builder. Along with importing the go_router.dart library, it's essential to also include a part directive that references the generated Dart file. The generated file will always have the name [source_file].g.dart.

import 'package:go_router/go_router.dart';

part 'readme_excerpts.g.dart';

Running build_runner

To do a one-time build:

dart run build_runner build

Read more about using build_runner on pub.dev.

Builder options

duplicate_route_paths

When two routes resolve to the same URL, go_router matches the first and the second becomes unreachable. Navigating to the second one's location shows the first one's page. The builder warns about this at build time.

Routes are compared by the URL they resolve to, not by the path they declare, so depth in the route tree does not matter. A route at section/detail collides with a route at section holding a child at detail. Parameter names are ignored, so product/:id and product/:productId are the same URL, though a parameter's regex constraint still counts, so product/:id(\d+) and product/:id(\w+) are not. Casing is ignored when the earlier route sets caseSensitive: false, since it then matches any casing.

The whole library is compared, part files included. Shell routes and StatefulShellRoute branches contribute nothing to the URLs beneath them.

Use build.yaml to change what a duplicate does:

targets:
  $default:
    builders:
      go_router_builder:
        options:
          duplicate_route_paths: error

Accepted values are warning (the default), error, and ignore.

Warning is the default because some duplicates work. Matching backtracks, so naming one route class twice at the same path, each declaration carrying different children, is fine. Both resolve to the same class and every child stays reachable, which makes it a way to group children by feature. The builder still warns, because it cannot tell that from a mistake, so use ignore if you write it deliberately. Their children are a different story: a child path repeated across the two declarations is a genuine collision and is reported on its own.

error applies to the whole package with no per-route exception, so it fails on the deliberate grouping too.

Migration Guides

Overview

go_router fundamentally relies on the ability to match a string-based location in a URI format into one or more page builders, each that require zero or more arguments that are passed as path and query parameters as part of the location. go_router does a good job of making the path and query parameters available via the pathParameters and queryParameters properties of the GoRouterState object, but often the page builder must first parse the parameters into types that aren't Strings, e.g.

GoRoute(
  path: ':familyId',
  builder: (BuildContext context, GoRouterState state) {
    // Require the familyId to be present and be an integer.
    final int familyId = int.parse(state.pathParameters['familyId']!);
    return FamilyScreen(familyId);
  },
);

In this example, the familyId parameter is a) required and b) must be an int. However, neither of these requirements are checked until run-time, making it easy to write code that is not type-safe, e.g.

void tap() =>
    context.go('/familyId/a42'); // This is an error: `a42` is not an `int`.

Dart's type system allows mistakes to be caught at compile-time instead of run-time. The goal of the routing is to provide a way to define the required and optional parameters that a specific route consumes and to use code generation to take out the drudgery of writing a bunch of go, push and location boilerplate code implementations ourselves.

Defining a route

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

class HomeRoute extends GoRouteData with $HomeRoute {
  const HomeRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

Route tree

The tree of routes is defined as an attribute on each of the top-level routes:

@TypedGoRoute<HomeRoute>(
  path: '/',
  routes: <TypedGoRoute<GoRouteData>>[
    TypedGoRoute<FamilyRoute>(path: 'family/:fid'),
  ],
)
class HomeRoute extends GoRouteData with $HomeRoute {
  const HomeRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}


class RedirectRoute extends GoRouteData {
  // There is no need to implement [build] when this [redirect] is unconditional.
  @override
  String? redirect(BuildContext context, GoRouterState state) {
    return const HomeRoute().location;
  }
}


@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData with $LoginRoute {
  LoginRoute({this.from});
  final String? from;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return LoginScreen(from: from);
  }
}

GoRouter initialization

The code generator aggregates all top-level routes into a single list called $appRoutes for use in initializing the GoRouter instance:

final router = GoRouter(routes: $appRoutes);

Error builder

One can use typed routes to provide an error builder as well:

class ErrorRoute extends GoRouteData {
  ErrorRoute({required this.error});
  final Exception error;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return ErrorScreen(error: error);
  }
}

With this in place, you can provide the errorBuilder parameter like so:

final routerWithErrorBuilder = GoRouter(
  routes: $appRoutes,
  errorBuilder: (BuildContext context, GoRouterState state) {
    return ErrorRoute(error: state.error!).build(context, state);
  },
);

Navigate using the go or push methods provided by the code generator:

void onTap() => const FamilyRoute(fid: 'f2').go(context);

If you get this wrong, the compiler will complain:

// This is an error: missing required parameter 'fid'.
void errorTap() => const FamilyRoute().go(context);

This is the point of typed routing: the error is found statically.

Return value

Starting from go_router 6.5.0, pushing a route and subsequently popping it, can produce a return value. The generated routes also follow this functionality.

final bool? result = await const FamilyRoute(
  fid: 'John',
).push<bool>(context);

Query parameters

Parameters (named or positional) not listed in the path of TypedGoRoute indicate query parameters:

@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData with $LoginRoute {
  LoginRoute({this.from});
  final String? from;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return LoginScreen(from: from);
  }
}

Default values

For query parameters with a non-nullable type, you can define a default value:

@TypedGoRoute<MyRoute>(path: '/my-route')
class MyRoute extends GoRouteData with $MyRoute {
  MyRoute({this.queryParameter = 'defaultValue'});
  final String queryParameter;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return MyScreen(queryParameter: queryParameter);
  }
}

A query parameter that equals to its default value is not included in the location.

Extra parameter

A route can consume an extra parameter by taking it as a typed constructor parameter with the special name $extra:

class PersonRouteWithExtra extends GoRouteData with $PersonRouteWithExtra {
  PersonRouteWithExtra(this.$extra);
  final Person? $extra;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return PersonScreen($extra);
  }
}

Pass the extra param as a typed object:

void tapWithExtra() {
  PersonRouteWithExtra(Person(id: 1, name: 'Marvin', age: 42)).go(context);
}

The $extra parameter is still passed outside the location, still defeats dynamic and deep linking (including the browser back button) and is still not recommended when targeting Flutter web.

Mixed parameters

You can, of course, combine the use of path, query and $extra parameters:

@TypedGoRoute<HotdogRouteWithEverything>(path: '/:ketchup')
class HotdogRouteWithEverything extends GoRouteData
    with $HotdogRouteWithEverything {
  HotdogRouteWithEverything(this.ketchup, this.mustard, this.$extra);
  final bool ketchup; // A required path parameter.
  final String? mustard; // An optional query parameter.
  final Sauce $extra; // A special $extra parameter.

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return HotdogScreen(ketchup, mustard, $extra);
  }
}

This seems kinda silly, but it works.

Redirection

Redirect using the location property on a route provided by the code generator:

redirect: (BuildContext context, GoRouterState state) {
  final bool loggedIn = loginInfo.loggedIn;
  final loggingIn = state.matchedLocation == LoginRoute().location;
  if (!loggedIn && !loggingIn) {
    return LoginRoute(from: state.matchedLocation).location;
  }
  if (loggedIn && loggingIn) {
    return const HomeRoute().location;
  }
  return null;
},

Route-level redirection

Handle route-level redirects by implementing the redirect method on the route:

class RedirectRoute extends GoRouteData {
  // There is no need to implement [build] when this [redirect] is unconditional.
  @override
  String? redirect(BuildContext context, GoRouterState state) {
    return const HomeRoute().location;
  }
}

Type conversions

The code generator can convert simple types like int, enum, and extension type to/from the String type of the underlying pathParameters:

enum BookKind { all, popular, recent }

@TypedGoRoute<BooksRoute>(path: '/books')
class BooksRoute extends GoRouteData with $BooksRoute {
  BooksRoute({this.kind = BookKind.popular});
  final BookKind kind;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return BooksScreen(kind: kind);
  }
}

Transitions

By default, the GoRouter will use the app it finds in the widget tree, e.g. MaterialApp, CupertinoApp, WidgetApp, etc. and use the corresponding page type to create the page that wraps the Widget returned by the route's build method, e.g. MaterialPage, CupertinoPage, NoTransitionPage, etc. Furthermore, it will use the state.pageKey property to set the key property of the page and the restorationId of the page.

Transition override

If you'd like to change how the page is created, e.g. to use a different page type, pass non-default parameters when creating the page (like a custom key) or access the GoRouteState object, you can override the buildPage method of the base class instead of the build method:

class MyMaterialRouteWithKey extends GoRouteData with $MyMaterialRouteWithKey {
  const MyMaterialRouteWithKey();
  static const LocalKey _key = ValueKey<String>('my-route-with-key');
  @override
  MaterialPage<void> buildPage(BuildContext context, GoRouterState state) {
    return const MaterialPage<void>(key: _key, child: MyPage());
  }
}

Custom transitions

Overriding the buildPage method is also useful for custom transitions:

class FancyRoute extends GoRouteData with $FancyRoute {
  const FancyRoute();
  @override
  CustomTransitionPage<void> buildPage(
    BuildContext context,
    GoRouterState state,
  ) {
    return CustomTransitionPage<void>(
      key: state.pageKey,
      child: const MyPage(),
      transitionsBuilder:
          (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) {
            return RotationTransition(turns: animation, child: child);
          },
    );
  }
}

TypedShellRoute and navigator keys

There may be situations where a child route of a shell needs to be displayed on a different navigator. This kind of scenarios can be achieved by declaring a static navigator key named:

  • $navigatorKey for ShellRoutes
  • $parentNavigatorKey for GoRoutes

Example:

final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();

@TypedShellRoute<MyShellRouteData>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<MyGoRouteData>(path: 'my-go-route'),
  ],
)
class MyShellRouteData extends ShellRouteData {
  const MyShellRouteData();

  static final GlobalKey<NavigatorState> $navigatorKey = shellNavigatorKey;

  @override
  Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
    return MyShellRoutePage(navigator);
  }
}

// For GoRoutes:
class MyGoRouteData extends GoRouteData with $MyGoRouteData {
  const MyGoRouteData();

  static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;

  @override
  Widget build(BuildContext context, GoRouterState state) => const MyPage();
}

An example is available here.

Relative routes

Relative routes allow reusing the same RouteData in different parts of the route tree. Define a relative route by extending RelativeGoRouteData.

@TypedRelativeGoRoute<DetailsRoute>(path: 'details')
class DetailsRoute extends RelativeGoRouteData with $DetailsRoute {
  const DetailsRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) =>
      const DetailsScreen();
}

Navigate using the goRelative or pushRelative methods provided by the code generator:

void onTapRelative() => const DetailsRoute().goRelative(context);

Relative routing methods are not idempotent and will cause an error when the relative location does not match a route.

Run tests

To run unit tests, run command dart tool/run_tests.dart from packages/go_router_builder/.

Each .dart file in test_inputs/ is a test case, paired with a .expect file holding either the generated output or the error message the builder must produce. Two optional companion files tune a case:

  • <name>.dart.options holds a JSON map of builder options, matching what build.yaml would pass to the builder.
  • <name>.dart.warnings holds the warnings the builder must log, one per line. An empty file asserts that the builder logs nothing.

To run tests in examples, run flutter test from packages/go_router_builder/example.

Libraries

go_router_builder
Configuration for using package:build-compatible build systems.