blob: 5d90b22f28519a34719385ee7d25e693fc2f4791 [file] [log] [blame] [view]
## Usage
### Dependencies
To use `go_router_builder`, you need to have the following dependencies in
`pubspec.yaml`.
```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
```
### 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`.
<?code-excerpt "example/lib/readme_excerpts.dart (import)"?>
```dart
import 'package:go_router/go_router.dart';
part 'readme_excerpts.g.dart';
```
### Running `build_runner`
To do a one-time build:
```console
dart run build_runner build
```
Read more about using
[`build_runner` on pub.dev](https://pub.dev/packages/build_runner).
## 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
`String`s, e.g.
<?code-excerpt "example/lib/readme_excerpts.dart (GoRoute)"?>
```dart
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.
<?code-excerpt "example/lib/readme_excerpts.dart (GoWrong)"?>
```dart
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.
<?code-excerpt "example/lib/readme_excerpts.dart (HomeRoute)"?>
```dart
class HomeRoute extends GoRouteData {
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:
<?code-excerpt "example/lib/readme_excerpts.dart (TypedGoRouteHomeRoute)"?>
```dart
@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` initialization
The code generator aggregates all top-level routes into a single list called
`$appRoutes` for use in initializing the `GoRouter` instance:
<?code-excerpt "example/lib/readme_excerpts.dart (GoRouter)"?>
```dart
final GoRouter router = GoRouter(routes: $appRoutes);
```
## Error builder
One can use typed routes to provide an error builder as well:
<?code-excerpt "example/lib/readme_excerpts.dart (ErrorRoute)"?>
```dart
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:
<?code-excerpt "example/lib/readme_excerpts.dart (routerWithErrorBuilder)"?>
```dart
final GoRouter routerWithErrorBuilder = GoRouter(
routes: $appRoutes,
errorBuilder: (BuildContext context, GoRouterState state) {
return ErrorRoute(error: state.error!).build(context, state);
},
);
```
## Navigation
Navigate using the `go` or `push` methods provided by the code generator:
<?code-excerpt "example/lib/readme_excerpts.dart (go)"?>
```dart
void onTap() => const FamilyRoute(fid: 'f2').go(context);
```
If you get this wrong, the compiler will complain:
<?code-excerpt "example/lib/readme_excerpts.dart (goError)"?>
```dart
// 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.
<?code-excerpt "example/lib/readme_excerpts.dart (awaitPush)"?>
```dart
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:
<?code-excerpt "example/lib/readme_excerpts.dart (login)"?>
```dart
@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);
}
}
```
### Default values
For query parameters with a **non-nullable** type, you can define a default value:
<?code-excerpt "example/lib/readme_excerpts.dart (MyRoute)"?>
```dart
@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.
## Extra parameter
A route can consume an extra parameter by taking it as a typed constructor
parameter with the special name `$extra`:
<?code-excerpt "example/lib/readme_excerpts.dart (PersonRouteWithExtra)"?>
```dart
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:
<?code-excerpt "example/lib/readme_excerpts.dart (tapWithExtra)"?>
```dart
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:
<?code-excerpt "example/lib/readme_excerpts.dart (HotdogRouteWithEverything)"?>
```dart
@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.
## Redirection
Redirect using the `location` property on a route provided by the code
generator:
<?code-excerpt "example/lib/readme_excerpts.dart (redirect)"?>
```dart
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;
},
```
## Route-level redirection
Handle route-level redirects by implementing the `redirect` method on the route:
<?code-excerpt "example/lib/readme_excerpts.dart (RedirectRoute)"?>
```dart
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` and `enum` to/from the
`String` type of the underlying pathParameters:
<?code-excerpt "example/lib/readme_excerpts.dart (BookKind)"?>
```dart
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);
}
}
```
## 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:
<?code-excerpt "example/lib/readme_excerpts.dart (MyMaterialRouteWithKey)"?>
```dart
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(),
);
}
}
```
### Custom transitions
Overriding the `buildPage` method is also useful for custom transitions:
<?code-excerpt "example/lib/readme_excerpts.dart (FancyRoute)"?>
```dart
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);
});
}
}
```
## 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:
<?code-excerpt "example/lib/readme_excerpts.dart (MyShellRouteData)"?>
```dart
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](https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/shell_route_with_keys_example.dart).
## Run tests
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`.