To use go_router_builder
, you need to have the following dependencies in pubspec.yaml
.
dependencies: # ...along with your other dependencies go_router: ^9.0.3 dev_dependencies: # ...along with your other dev-dependencies build_runner: ^2.0.0 go_router_builder: ^2.3.0
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';
build_runner
To do a one-time build:
dart run build_runner build
Read more about using build_runner
on pub.dev.
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 String
s, 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.
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(); }
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 { 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 { LoginRoute({this.from}); final String? from; @override Widget build(BuildContext context, GoRouterState state) { return LoginScreen(from: from); } }
GoRouter
initializationThe code generator aggregates all top-level routes into a single list called $appRoutes
for use in initializing the GoRouter
instance:
final GoRouter router = GoRouter(routes: $appRoutes);
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 GoRouter 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.
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);
Parameters (named or positional) not listed in the path of TypedGoRoute
indicate query parameters:
@TypedGoRoute<LoginRoute>(path: '/login') class LoginRoute extends GoRouteData { LoginRoute({this.from}); final String? from; @override Widget build(BuildContext context, GoRouterState state) { return LoginScreen(from: from); } }
For query parameters with a non-nullable type, you can define a default value:
@TypedGoRoute<MyRoute>(path: '/my-route') class MyRoute extends GoRouteData { 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.
A route can consume an extra parameter by taking it as a typed constructor parameter with the special name $extra
:
class PersonRouteWithExtra extends GoRouteData { 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.
You can, of course, combine the use of path, query and $extra parameters:
@TypedGoRoute<HotdogRouteWithEverything>(path: '/:ketchup') class HotdogRouteWithEverything extends GoRouteData { 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.
Redirect using the location
property on a route provided by the code generator:
redirect: (BuildContext context, GoRouterState state) { final bool loggedIn = loginInfo.loggedIn; final bool loggingIn = state.matchedLocation == LoginRoute().location; if (!loggedIn && !loggingIn) { return LoginRoute(from: state.matchedLocation).location; } if (loggedIn && loggingIn) { return const HomeRoute().location; } return null; },
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; } }
The code generator can convert simple types like int
and enum
to/from the String
type of the underlying pathParameters:
enum BookKind { all, popular, recent } class BooksRoute extends GoRouteData { BooksRoute({this.kind = BookKind.popular}); final BookKind kind; @override Widget build(BuildContext context, GoRouterState state) { return BooksScreen(kind: kind); } }
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.
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 { 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(), ); } }
Overriding the buildPage
method is also useful for custom transitions:
class FancyRoute extends GoRouteData { @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); }); } }
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 GoRoutesExample:
final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(); 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 { const MyGoRouteData(); static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey; @override Widget build(BuildContext context, GoRouterState state) => const MyPage(); }
An example is available here.
To run unit tests, run command dart tool/run_tests.dart
from packages/go_router_builder/
.
To run tests in examples, run flutter test
from packages/go_router_builder/example
.