Skip to content

Commit 27a2302

Browse files
authored
[go_router]: Add RelativeGoRouteData and TypedRelativeGoRoute (#9732)
Adds `RelativeGoRouteData` and `TypedRelativeGoRoute` to support type-safe relative routes. Foundation for #9749 which adds relative route support in `go_router_builder`. Continuation of #8476 by @ThangVuNguyenViet Compared to #8476, the approach taken in this PR avoids breaking changes and creates a separate `RouteData` subclass instead of extending `GoRouteData`. The approach increases flexibility and avoids extending behavior that might not make sense for a relative route. Necessary to fully resolve flutter/flutter#108177. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent f8c4100 commit 27a2302

File tree

4 files changed

+445
-94
lines changed

4 files changed

+445
-94
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
## NEXT
1+
## 16.2.0
22

3-
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.
3+
- Adds `RelativeGoRouteData` and `TypedRelativeGoRoute`.
4+
- Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.
45

56
## 16.1.0
7+
68
- Adds annotation for go_router_builder that enable custom string encoder/decoder [#110781](https://github.com/flutter/flutter/issues/110781). **Requires go_router_builder >= 3.1.0**.
79

810
## 16.0.0

packages/go_router/lib/src/route_data.dart

Lines changed: 208 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
88
import 'package:meta/meta.dart';
99
import 'package:meta/meta_meta.dart';
1010

11+
import 'configuration.dart';
1112
import 'route.dart';
1213
import 'state.dart';
1314

@@ -18,17 +19,10 @@ abstract class RouteData {
1819
const RouteData();
1920
}
2021

21-
/// A class to represent a [GoRoute] in
22-
/// [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html).
23-
///
24-
/// Subclasses must override one of [build], [buildPage], or
25-
/// [redirect].
26-
/// {@category Type-safe routes}
27-
abstract class GoRouteData extends RouteData {
28-
/// Allows subclasses to have `const` constructors.
29-
///
30-
/// [GoRouteData] is abstract and cannot be instantiated directly.
31-
const GoRouteData();
22+
/// A base class for [GoRouteData] and [RelativeGoRouteData] that provides
23+
/// common functionality for type-safe routing.
24+
abstract class _GoRouteDataBase extends RouteData {
25+
const _GoRouteDataBase();
3226

3327
/// Creates the [Widget] for `this` route.
3428
///
@@ -68,17 +62,93 @@ abstract class GoRouteData extends RouteData {
6862
/// Corresponds to [GoRoute.onExit].
6963
FutureOr<bool> onExit(BuildContext context, GoRouterState state) => true;
7064

65+
/// The error thrown when a user-facing method is not implemented by the
66+
/// generated code.
67+
static UnimplementedError get shouldBeGeneratedError => UnimplementedError(
68+
'Should be generated using [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html).',
69+
);
70+
71+
/// Used to cache [_GoRouteDataBase] that corresponds to a given [GoRouterState]
72+
/// to minimize the number of times it has to be deserialized.
73+
static final Expando<_GoRouteDataBase> stateObjectExpando =
74+
Expando<_GoRouteDataBase>('GoRouteState to _GoRouteDataBase expando');
75+
}
76+
77+
/// Helper to build a location string from a path and query parameters.
78+
String _buildLocation(String path, {Map<String, dynamic>? queryParams}) =>
79+
Uri.parse(path)
80+
.replace(
81+
queryParameters:
82+
// Avoid `?` in generated location if `queryParams` is empty
83+
queryParams?.isNotEmpty ?? false ? queryParams : null,
84+
)
85+
.toString();
86+
87+
/// Holds the parameters for constructing a [GoRoute].
88+
class _GoRouteParameters {
89+
const _GoRouteParameters({
90+
required this.builder,
91+
required this.pageBuilder,
92+
required this.redirect,
93+
required this.onExit,
94+
});
95+
96+
final GoRouterWidgetBuilder builder;
97+
final GoRouterPageBuilder pageBuilder;
98+
final GoRouterRedirect redirect;
99+
final ExitCallback onExit;
100+
}
101+
102+
/// Helper to create [GoRoute] parameters from a factory function and an Expando.
103+
_GoRouteParameters _createGoRouteParameters<T extends _GoRouteDataBase>({
104+
required T Function(GoRouterState) factory,
105+
required Expando<_GoRouteDataBase> expando,
106+
}) {
107+
T factoryImpl(GoRouterState state) {
108+
final Object? extra = state.extra;
109+
110+
// If the "extra" value is of type `T` then we know it's the source
111+
// instance, so it doesn't need to be recreated.
112+
if (extra is T) {
113+
return extra;
114+
}
115+
116+
return (expando[state] ??= factory(state)) as T;
117+
}
118+
119+
return _GoRouteParameters(
120+
builder:
121+
(BuildContext context, GoRouterState state) =>
122+
factoryImpl(state).build(context, state),
123+
pageBuilder:
124+
(BuildContext context, GoRouterState state) =>
125+
factoryImpl(state).buildPage(context, state),
126+
redirect:
127+
(BuildContext context, GoRouterState state) =>
128+
factoryImpl(state).redirect(context, state),
129+
onExit:
130+
(BuildContext context, GoRouterState state) =>
131+
factoryImpl(state).onExit(context, state),
132+
);
133+
}
134+
135+
/// A class to represent a [GoRoute] in
136+
/// [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html).
137+
///
138+
/// Subclasses must override one of [build], [buildPage], or
139+
/// [redirect].
140+
/// {@category Type-safe routes}
141+
abstract class GoRouteData extends _GoRouteDataBase {
142+
/// Allows subclasses to have `const` constructors.
143+
///
144+
/// [GoRouteData] is abstract and cannot be instantiated directly.
145+
const GoRouteData();
146+
71147
/// A helper function used by generated code.
72148
///
73149
/// Should not be used directly.
74150
static String $location(String path, {Map<String, dynamic>? queryParams}) =>
75-
Uri.parse(path)
76-
.replace(
77-
queryParameters:
78-
// Avoid `?` in generated location if `queryParams` is empty
79-
queryParams?.isNotEmpty ?? false ? queryParams : null,
80-
)
81-
.toString();
151+
_buildLocation(path, queryParams: queryParams);
82152

83153
/// A helper function used by generated code.
84154
///
@@ -91,72 +161,120 @@ abstract class GoRouteData extends RouteData {
91161
GlobalKey<NavigatorState>? parentNavigatorKey,
92162
List<RouteBase> routes = const <RouteBase>[],
93163
}) {
94-
T factoryImpl(GoRouterState state) {
95-
final Object? extra = state.extra;
164+
final _GoRouteParameters params = _createGoRouteParameters<T>(
165+
factory: factory,
166+
expando: _GoRouteDataBase.stateObjectExpando,
167+
);
96168

97-
// If the "extra" value is of type `T` then we know it's the source
98-
// instance of `GoRouteData`, so it doesn't need to be recreated.
99-
if (extra is T) {
100-
return extra;
101-
}
169+
return GoRoute(
170+
path: path,
171+
name: name,
172+
caseSensitive: caseSensitive,
173+
builder: params.builder,
174+
pageBuilder: params.pageBuilder,
175+
redirect: params.redirect,
176+
routes: routes,
177+
parentNavigatorKey: parentNavigatorKey,
178+
onExit: params.onExit,
179+
);
180+
}
102181

103-
return (_stateObjectExpando[state] ??= factory(state)) as T;
104-
}
182+
/// The location of this route, e.g. /family/f2/person/p1
183+
String get location => throw _GoRouteDataBase.shouldBeGeneratedError;
184+
185+
/// Navigate to the route.
186+
void go(BuildContext context) =>
187+
throw _GoRouteDataBase.shouldBeGeneratedError;
105188

106-
Widget builder(BuildContext context, GoRouterState state) =>
107-
factoryImpl(state).build(context, state);
189+
/// Push the route onto the page stack.
190+
Future<T?> push<T>(BuildContext context) =>
191+
throw _GoRouteDataBase.shouldBeGeneratedError;
108192

109-
Page<void> pageBuilder(BuildContext context, GoRouterState state) =>
110-
factoryImpl(state).buildPage(context, state);
193+
/// Replaces the top-most page of the page stack with the route.
194+
void pushReplacement(BuildContext context) =>
195+
throw _GoRouteDataBase.shouldBeGeneratedError;
111196

112-
FutureOr<String?> redirect(BuildContext context, GoRouterState state) =>
113-
factoryImpl(state).redirect(context, state);
197+
/// Replaces the top-most page of the page stack with the route but treats
198+
/// it as the same page.
199+
///
200+
/// The page key will be reused. This will preserve the state and not run any
201+
/// page animation.
202+
///
203+
void replace(BuildContext context) =>
204+
throw _GoRouteDataBase.shouldBeGeneratedError;
205+
}
114206

115-
FutureOr<bool> onExit(BuildContext context, GoRouterState state) =>
116-
factoryImpl(state).onExit(context, state);
207+
/// A class to represent a relative [GoRoute] in
208+
/// [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html).
209+
///
210+
/// Subclasses must override one of [build], [buildPage], or
211+
/// [redirect].
212+
/// {@category Type-safe routes}
213+
abstract class RelativeGoRouteData extends _GoRouteDataBase {
214+
/// Allows subclasses to have `const` constructors.
215+
///
216+
/// [RelativeGoRouteData] is abstract and cannot be instantiated directly.
217+
const RelativeGoRouteData();
218+
219+
/// A helper function used by generated code.
220+
///
221+
/// Should not be used directly.
222+
static String $location(String path, {Map<String, dynamic>? queryParams}) =>
223+
_buildLocation(path, queryParams: queryParams);
224+
225+
/// A helper function used by generated code.
226+
///
227+
/// Should not be used directly.
228+
static GoRoute $route<T extends RelativeGoRouteData>({
229+
required String path,
230+
bool caseSensitive = true,
231+
required T Function(GoRouterState) factory,
232+
GlobalKey<NavigatorState>? parentNavigatorKey,
233+
List<RouteBase> routes = const <RouteBase>[],
234+
}) {
235+
final _GoRouteParameters params = _createGoRouteParameters<T>(
236+
factory: factory,
237+
expando: _GoRouteDataBase.stateObjectExpando,
238+
);
117239

118240
return GoRoute(
119241
path: path,
120-
name: name,
121242
caseSensitive: caseSensitive,
122-
builder: builder,
123-
pageBuilder: pageBuilder,
124-
redirect: redirect,
243+
builder: params.builder,
244+
pageBuilder: params.pageBuilder,
245+
redirect: params.redirect,
125246
routes: routes,
126247
parentNavigatorKey: parentNavigatorKey,
127-
onExit: onExit,
248+
onExit: params.onExit,
128249
);
129250
}
130251

131-
/// Used to cache [GoRouteData] that corresponds to a given [GoRouterState]
132-
/// to minimize the number of times it has to be deserialized.
133-
static final Expando<GoRouteData> _stateObjectExpando = Expando<GoRouteData>(
134-
'GoRouteState to GoRouteData expando',
135-
);
252+
/// The sub-location of this route, e.g. person/p1
253+
String get subLocation => throw _GoRouteDataBase.shouldBeGeneratedError;
136254

137-
/// The location of this route.
138-
String get location => throw _shouldBeGeneratedError;
255+
/// The relative location of this route, e.g. ./person/p1
256+
String get relativeLocation => throw _GoRouteDataBase.shouldBeGeneratedError;
139257

140258
/// Navigate to the route.
141-
void go(BuildContext context) => throw _shouldBeGeneratedError;
259+
void goRelative(BuildContext context) =>
260+
throw _GoRouteDataBase.shouldBeGeneratedError;
142261

143262
/// Push the route onto the page stack.
144-
Future<T?> push<T>(BuildContext context) => throw _shouldBeGeneratedError;
263+
Future<T?> pushRelative<T>(BuildContext context) =>
264+
throw _GoRouteDataBase.shouldBeGeneratedError;
145265

146266
/// Replaces the top-most page of the page stack with the route.
147-
void pushReplacement(BuildContext context) => throw _shouldBeGeneratedError;
267+
void pushReplacementRelative(BuildContext context) =>
268+
throw _GoRouteDataBase.shouldBeGeneratedError;
148269

149270
/// Replaces the top-most page of the page stack with the route but treats
150271
/// it as the same page.
151272
///
152273
/// The page key will be reused. This will preserve the state and not run any
153274
/// page animation.
154275
///
155-
void replace(BuildContext context) => throw _shouldBeGeneratedError;
156-
157-
static UnimplementedError get _shouldBeGeneratedError => UnimplementedError(
158-
'Should be generated using [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html).',
159-
);
276+
void replaceRelative(BuildContext context) =>
277+
throw _GoRouteDataBase.shouldBeGeneratedError;
160278
}
161279

162280
/// A class to represent a [ShellRoute] in
@@ -402,6 +520,41 @@ class TypedGoRoute<T extends GoRouteData> extends TypedRoute<T> {
402520
final bool caseSensitive;
403521
}
404522

523+
/// A superclass for each typed relative go route descendant
524+
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
525+
class TypedRelativeGoRoute<T extends RelativeGoRouteData>
526+
extends TypedRoute<T> {
527+
/// Default const constructor
528+
const TypedRelativeGoRoute({
529+
required this.path,
530+
this.routes = const <TypedRoute<RouteData>>[],
531+
this.caseSensitive = true,
532+
});
533+
534+
/// The relative path that corresponds to this route.
535+
///
536+
/// See [GoRoute.path].
537+
///
538+
///
539+
final String path;
540+
541+
/// Child route definitions.
542+
///
543+
/// See [RouteBase.routes].
544+
final List<TypedRoute<RouteData>> routes;
545+
546+
/// Determines whether the route matching is case sensitive.
547+
///
548+
/// When `true`, the path must match the specified case. For example,
549+
/// a route with `path: '/family/:fid'` will not match `/FaMiLy/f2`.
550+
///
551+
/// When `false`, the path matching is case insensitive. The route
552+
/// with `path: '/family/:fid'` will match `/FaMiLy/f2`.
553+
///
554+
/// Defaults to `true`.
555+
final bool caseSensitive;
556+
}
557+
405558
/// A superclass for each typed shell route descendant
406559
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
407560
class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {

packages/go_router/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 16.1.0
4+
version: 16.2.0
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

0 commit comments

Comments
 (0)