blob: 0eff1f07c6a093f0232f8a09e2623aed14df4409 [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: ^7.0.0
dev_dependencies:
# ...along with your other dev-dependencies
build_runner: ^2.0.0
go_router_builder: ^2.0.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`.
```dart
import 'package:go_router/go_router.dart';
part 'this_file.g.dart';
```
### Running `build_runner`
To do a one-time build:
```console
flutter pub 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.
```dart
GoRoute(
path: ':authorId',
builder: (context, state) {
// require the authorId to be present and be an integer
final authorId = int.parse(state.pathParameters['authorId']!);
return AuthorDetailsScreen(authorId: authorId);
},
),
```
In this example, the `authorId` 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.
```dart
void _tap() => context.go('/author/a42'); // 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.
```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:
```dart
@TypedGoRoute<HomeRoute>(
path: '/',
routes: <TypedGoRoute<GoRouteData>>[
TypedGoRoute<FamilyRoute>(
path: 'family/:familyId',
)
],
)
class HomeRoute extends GoRouteData {
const HomeRoute();
@override
Widget build(BuildContext context, GoRouterState state) => HomeScreen(families: familyData);
}
@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData {...}
```
## `GoRouter` initialization
The code generator aggregates all top-level routes into a single list called
`$appRoutes` for use in initializing the `GoRouter` instance:
```dart
final _router = GoRouter(routes: $appRoutes);
```
## Error builder
One can use typed routes to provide an error builder as well:
```dart
class ErrorRoute extends GoRouteData {
ErrorRoute({required this.error});
final Exception error;
@override
Widget build(BuildContext context, GoRouterState state) => ErrorScreen(error: error);
}
```
With this in place, you can provide the `errorBuilder` parameter like so:
```dart
final _router = GoRouter(
routes: $appRoutes,
errorBuilder: (c, s) => ErrorRoute(s.error!).build(c),
);
```
## Navigation
Navigate using the `go` or `push` methods provided by the code generator:
```dart
void _tap() => PersonRoute(fid: 'f2', pid: 'p1').go(context);
```
If you get this wrong, the compiler will complain:
```dart
// error: missing required parameter 'fid'
void _tap() => PersonRoute(pid: 'p1').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.
```dart
void _tap() async {
final result = await PersonRoute(pid: 'p1').go(context);
}
```
## Query parameters
Parameters (named or positional) not listed in the path of `TypedGoRoute` indicate query parameters:
```dart
@TypedGoRoute(path: '/login')
class LoginRoute extends GoRouteData {
LoginRoute({this.from});
final String? from;
@override
Widget build(BuildContext context, GoRouterState state) => LoginScreen(from: from);
}
```
### Default values
For query parameters with a **non-nullable** type, you can define a default value:
```dart
@TypedGoRoute(path: '/my-route')
class MyRoute extends GoRouteData {
MyRoute({this.queryParameter = 'defaultValue'});
final String queryParameter;
@override
Widget build(BuildContext context, GoRouterState state) => 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`:
```dart
class PersonRouteWithExtra extends GoRouteData {
PersonRouteWithExtra({this.$extra});
final int? $extra;
@override
Widget build(BuildContext context, GoRouterState state) => PersonScreen(personId: $extra);
}
```
Pass the extra param as a typed object:
```dart
void _tap() => PersonRouteWithExtra(Person(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:
```dart
@TypedGoRoute<HotdogRouteWithEverything>(path: '/:ketchup')
class HotdogRouteWithEverything extends GoRouteData {
HotdogRouteWithEverything(this.ketchup, this.mustard, this.$extra);
final bool ketchup; // required path parameter
final String? mustard; // optional query parameter
final Sauce $extra; // special $extra parameter
@override
Widget build(BuildContext context, GoRouterState state) => 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:
```dart
redirect: (state) {
final loggedIn = loginInfo.loggedIn;
final loggingIn = state.matchedLocation == LoginRoute().location;
if( !loggedIn && !loggingIn ) return LoginRoute(from: state.matchedLocation).location;
if( loggedIn && loggingIn ) return HomeRoute().location;
return null;
}
```
## Route-level redirection
Handle route-level redirects by implementing the `redirect` method on the route:
```dart
class HomeRoute extends GoRouteData {
// no need to implement [build] when this [redirect] is unconditional
@override
String? redirect(BuildContext context, GoRouterState state) => BooksRoute().location;
}
```
## Type conversions
The code generator can convert simple types like `int` and `enum` to/from the
`String` type of the underlying pathParameters:
```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) => 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:
```dart
class MyMaterialRouteWithKey extends GoRouteData {
static final _key = LocalKey('my-route-with-key');
@override
MaterialPage<void> buildPage(BuildContext context, GoRouterState state) =>
MaterialPage<void>(
key: _key,
child: MyPage(),
);
}
```
### Custom transitions
Overriding the `buildPage` method is also useful for custom transitions:
```dart
class FancyRoute extends GoRouteData {
@override
MaterialPage<void> buildPage(BuildContext context, GoRouterState state) =>
CustomTransitionPage<void>(
key: state.pageKey,
child: FancyPage(),
transitionsBuilder: (context, animation, animation2, child) =>
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:
```dart
// For ShellRoutes:
final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>();
class MyShellRouteData extends ShellRouteData {
const MyShellRouteData();
static final GlobalKey<NavigatorState> $navigatorKey = shellNavigatorKey;
@override
Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
// ...
}
}
// For GoRoutes:
class MyGoRouteData extends GoRouteData {
const MyGoRouteData();
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Widget build(BuildContext context, GoRouterState state) {
// ...
}
}
```
An example is available [here](https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/shell_route_with_keys_example.dart).