[go_router_builder] Accept required parameters not in path (#4039)
This PR allows required/positional parameters to not be in the path.
Fixes https://github.com/flutter/flutter/issues/126796
*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md
index 7ebb576..81a4e7d 100644
--- a/packages/go_router_builder/CHANGELOG.md
+++ b/packages/go_router_builder/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.1.0
+
+* Supports required/positional parameters that are not in the path.
+
## 2.0.2
* Fixes unawaited_futures violations.
diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md
index 18a4ea0..0eff1f0 100644
--- a/packages/go_router_builder/README.md
+++ b/packages/go_router_builder/README.md
@@ -89,9 +89,6 @@
}
```
-Required parameters are pulled from the route's `path` defined in the route
-tree.
-
## Route tree
The tree of routes is defined as an attribute on each of the top-level routes:
@@ -178,9 +175,10 @@
## Query parameters
-Optional parameters (named or positional) indicate 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;
@@ -195,6 +193,7 @@
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;
@@ -237,6 +236,7 @@
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
diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart
index a67f33b..3e53cb1 100644
--- a/packages/go_router_builder/lib/src/route_config.dart
+++ b/packages/go_router_builder/lib/src/route_config.dart
@@ -369,21 +369,15 @@
String _decodeFor(ParameterElement element) {
if (element.isRequired) {
- if (element.type.nullabilitySuffix == NullabilitySuffix.question) {
+ if (element.type.nullabilitySuffix == NullabilitySuffix.question &&
+ _pathParams.contains(element.name)) {
throw InvalidGenerationSourceError(
- 'Required parameters cannot be nullable.',
- element: element,
- );
- }
-
- if (!_pathParams.contains(element.name) && !element.isExtraField) {
- throw InvalidGenerationSourceError(
- 'Missing param `${element.name}` in path.',
+ 'Required parameters in the path cannot be nullable.',
element: element,
);
}
}
- final String fromStateExpression = decodeParameter(element);
+ final String fromStateExpression = decodeParameter(element, _pathParams);
if (element.isPositional) {
return '$fromStateExpression,';
diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart
index 1ed8507..cea332a 100644
--- a/packages/go_router_builder/lib/src/type_helpers.dart
+++ b/packages/go_router_builder/lib/src/type_helpers.dart
@@ -44,15 +44,15 @@
/// Returns the decoded [String] value for [element], if its type is supported.
///
/// Otherwise, throws an [InvalidGenerationSourceError].
-String decodeParameter(ParameterElement element) {
+String decodeParameter(ParameterElement element, Set<String> pathParameters) {
if (element.isExtraField) {
- return 'state.${_stateValueAccess(element)}';
+ return 'state.${_stateValueAccess(element, pathParameters)}';
}
final DartType paramType = element.type;
for (final _TypeHelper helper in _helpers) {
if (helper._matchesType(paramType)) {
- String decoded = helper._decode(element);
+ String decoded = helper._decode(element, pathParameters);
if (element.isOptional && element.hasDefaultValue) {
if (element.type.isNullableType) {
throw NullableDefaultValueError(element);
@@ -92,30 +92,30 @@
// ignore: deprecated_member_use
String enumMapName(InterfaceType type) => '_\$${type.element.name}EnumMap';
-String _stateValueAccess(ParameterElement element) {
+String _stateValueAccess(ParameterElement element, Set<String> pathParameters) {
if (element.isExtraField) {
return 'extra as ${element.type.getDisplayString(withNullability: element.isOptional)}';
}
- if (element.isRequired) {
- return 'pathParameters[${escapeDartString(element.name)}]!';
+ late String access;
+ if (pathParameters.contains(element.name)) {
+ access = 'pathParameters[${escapeDartString(element.name)}]';
+ } else {
+ access = 'queryParameters[${escapeDartString(element.name.kebab)}]';
+ }
+ if (pathParameters.contains(element.name) ||
+ (!element.type.isNullableType && !element.hasDefaultValue)) {
+ access += '!';
}
- if (element.isOptional) {
- return 'queryParameters[${escapeDartString(element.name.kebab)}]';
- }
-
- throw InvalidGenerationSourceError(
- '$likelyIssueMessage (param not required or optional)',
- element: element,
- );
+ return access;
}
abstract class _TypeHelper {
const _TypeHelper();
/// Decodes the value from its string representation in the URL.
- String _decode(ParameterElement parameterElement);
+ String _decode(ParameterElement parameterElement, Set<String> pathParameters);
/// Encodes the value from its string representation in the URL.
String _encode(String fieldName, DartType type);
@@ -228,8 +228,9 @@
const _TypeHelperString();
@override
- String _decode(ParameterElement parameterElement) =>
- 'state.${_stateValueAccess(parameterElement)}';
+ String _decode(
+ ParameterElement parameterElement, Set<String> pathParameters) =>
+ 'state.${_stateValueAccess(parameterElement, pathParameters)}';
@override
String _encode(String fieldName, DartType type) => fieldName;
@@ -257,7 +258,8 @@
const _TypeHelperIterable();
@override
- String _decode(ParameterElement parameterElement) {
+ String _decode(
+ ParameterElement parameterElement, Set<String> pathParameters) {
if (parameterElement.type is ParameterizedType) {
final DartType iterableType =
(parameterElement.type as ParameterizedType).typeArguments.first;
@@ -323,17 +325,20 @@
String helperName(DartType paramType);
@override
- String _decode(ParameterElement parameterElement) {
+ String _decode(
+ ParameterElement parameterElement, Set<String> pathParameters) {
final DartType paramType = parameterElement.type;
+ final String parameterName = parameterElement.name;
- if (!parameterElement.isRequired) {
+ if (!pathParameters.contains(parameterName) &&
+ (paramType.isNullableType || parameterElement.hasDefaultValue)) {
return '$convertMapValueHelperName('
- '${escapeDartString(parameterElement.name.kebab)}, '
+ '${escapeDartString(parameterName.kebab)}, '
'state.queryParameters, '
'${helperName(paramType)})';
}
return '${helperName(paramType)}'
- '(state.${_stateValueAccess(parameterElement)})';
+ '(state.${_stateValueAccess(parameterElement, pathParameters)})';
}
}
diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml
index 322428d..027b79e 100644
--- a/packages/go_router_builder/pubspec.yaml
+++ b/packages/go_router_builder/pubspec.yaml
@@ -2,7 +2,7 @@
description: >-
A builder that supports generated strongly-typed route helpers for
package:go_router
-version: 2.0.2
+version: 2.1.0
repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22
diff --git a/packages/go_router_builder/test/builder_test.dart b/packages/go_router_builder/test/builder_test.dart
index 056d854..53df1f0 100644
--- a/packages/go_router_builder/test/builder_test.dart
+++ b/packages/go_router_builder/test/builder_test.dart
@@ -26,10 +26,11 @@
'BadPathParam',
'ExtraValueRoute',
'RequiredExtraValueRoute',
- 'MissingPathParam',
'MissingPathValue',
'MissingTypeAnnotation',
- 'NullableRequiredParam',
+ 'NullableRequiredParamInPath',
+ 'NullableRequiredParamNotInPath',
+ 'NonNullableRequiredParamNotInPath',
'UnsupportedType',
'theAnswer',
'EnumParam',
diff --git a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart
index 678a528..016d2c8 100644
--- a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart
+++ b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart
@@ -45,21 +45,83 @@
}
@ShouldThrow(
- 'Required parameters cannot be nullable.',
+ 'Required parameters in the path cannot be nullable.',
)
-@TypedGoRoute<NullableRequiredParam>(path: 'bob/:id')
-class NullableRequiredParam extends GoRouteData {
- NullableRequiredParam({required this.id});
+@TypedGoRoute<NullableRequiredParamInPath>(path: 'bob/:id')
+class NullableRequiredParamInPath extends GoRouteData {
+ NullableRequiredParamInPath({required this.id});
final int? id;
}
-@ShouldThrow(
- 'Missing param `id` in path.',
-)
-@TypedGoRoute<MissingPathParam>(path: 'bob/')
-class MissingPathParam extends GoRouteData {
- MissingPathParam({required this.id});
- final String id;
+@ShouldGenerate(r'''
+RouteBase get $nullableRequiredParamNotInPath => GoRouteData.$route(
+ path: 'bob',
+ factory: $NullableRequiredParamNotInPathExtension._fromState,
+ );
+
+extension $NullableRequiredParamNotInPathExtension
+ on NullableRequiredParamNotInPath {
+ static NullableRequiredParamNotInPath _fromState(GoRouterState state) =>
+ NullableRequiredParamNotInPath(
+ id: _$convertMapValue('id', state.queryParameters, int.parse),
+ );
+
+ String get location => GoRouteData.$location(
+ 'bob',
+ );
+
+ void go(BuildContext context) => context.go(location);
+
+ Future<T?> push<T>(BuildContext context) => context.push<T>(location);
+
+ void pushReplacement(BuildContext context) =>
+ context.pushReplacement(location);
+}
+
+T? _$convertMapValue<T>(
+ String key,
+ Map<String, String> map,
+ T Function(String) converter,
+) {
+ final value = map[key];
+ return value == null ? null : converter(value);
+}
+''')
+@TypedGoRoute<NullableRequiredParamNotInPath>(path: 'bob')
+class NullableRequiredParamNotInPath extends GoRouteData {
+ NullableRequiredParamNotInPath({required this.id});
+ final int? id;
+}
+
+@ShouldGenerate(r'''
+RouteBase get $nonNullableRequiredParamNotInPath => GoRouteData.$route(
+ path: 'bob',
+ factory: $NonNullableRequiredParamNotInPathExtension._fromState,
+ );
+
+extension $NonNullableRequiredParamNotInPathExtension
+ on NonNullableRequiredParamNotInPath {
+ static NonNullableRequiredParamNotInPath _fromState(GoRouterState state) =>
+ NonNullableRequiredParamNotInPath(
+ id: int.parse(state.queryParameters['id']!),
+ );
+
+ String get location => GoRouteData.$location(
+ 'bob',
+ );
+
+ void go(BuildContext context) => context.go(location);
+
+ Future<T?> push<T>(BuildContext context) => context.push<T>(location);
+
+ void pushReplacement(BuildContext context) =>
+ context.pushReplacement(location);
+}
+''')
+@TypedGoRoute<NonNullableRequiredParamNotInPath>(path: 'bob')
+class NonNullableRequiredParamNotInPath extends GoRouteData {
+ NonNullableRequiredParamNotInPath({required this.id});
+ final int id;
}
@ShouldGenerate(r'''