From d98480c4bf2b94980bdf82d2b85879d41deec11b Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 19 Feb 2026 13:56:51 +0800 Subject: [PATCH 01/11] Support custom types --- .../lib/src/route_config.dart | 4 +- .../lib/src/type_helpers.dart | 167 +++++++++++++++--- 2 files changed, 139 insertions(+), 32 deletions(-) diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 03316a9c8440..4ee1e96c507d 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -323,9 +323,7 @@ mixin _GoRouteMixin on RouteBaseConfig { if (param.type.isNullableType) { throw NullableDefaultValueError(param); } - conditions.add( - compareField(param, parameterName, param.defaultValueCode!), - ); + conditions.add(compareField(param)); } else if (param.type.isNullableType) { conditions.add('$selfFieldName.$parameterName != null'); } diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index 173f9c6b6a51..e05d069fa1ff 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -94,30 +94,50 @@ String decodeParameter( } final DartType paramType = element.type; - for (final _TypeHelper helper in _helpers) { - if (helper._matchesType(paramType)) { - String? decoder; + final String? annotatedDecoder = element.decoder; + + if (annotatedDecoder != null) { + // If there is a custom decoder, use it directly. + var decoded = + '$annotatedDecoder(state.${_stateValueAccess(element, pathParameters)})'; + if (element.isOptional && element.hasDefaultValue) { + if (element.type.isNullableType) { + throw NullableDefaultValueError(element); + } + decoded += ' ?? ${element.defaultValueCode!}'; + } + return decoded; + } - final ElementAnnotation? annotation = metadata?.firstWhereOrNull(( - ElementAnnotation annotation, - ) { - return annotation.computeConstantValue()?.type?.getDisplayString() == - 'CustomParameterCodec'; - }); - if (annotation != null) { - final String? decode = _getCustomCodec(annotation, 'decode'); - final String? encode = _getCustomCodec(annotation, 'encode'); - if (decode != null && encode != null) { - decoder = decode; - } else { - throw InvalidGenerationSourceError( - 'The parameter type ' - '`${withoutNullability(paramType.getDisplayString())}` not have a well defined CustomParameterCodec decorator.', - element: element, - ); + for (final _TypeHelper helper in _helpers) { + if (annotatedDecoder != null || helper._matchesType(paramType)) { + var decoder = annotatedDecoder; + String decoded; + if (decoder != null) { + decoded = ''; + } else { + final ElementAnnotation? annotation = metadata?.firstWhereOrNull(( + ElementAnnotation annotation, + ) { + return annotation.computeConstantValue()?.type?.getDisplayString() == + 'CustomParameterCodec'; + }); + if (annotation != null) { + final String? decode = _getCustomCodec(annotation, 'decode'); + final String? encode = _getCustomCodec(annotation, 'encode'); + if (decode != null && encode != null) { + decoder = decode; + } else { + throw InvalidGenerationSourceError( + 'The parameter type ' + '`${withoutNullability(paramType.getDisplayString())}` not have a well defined CustomParameterCodec decorator.', + element: element, + ); + } } + decoded = helper._decode(element, pathParameters, decoder); } - String decoded = helper._decode(element, pathParameters, decoder); + if (element.isOptional && element.hasDefaultValue) { if (element.type.isNullableType) { throw NullableDefaultValueError(element); @@ -133,7 +153,8 @@ String decodeParameter( throw InvalidGenerationSourceError( 'The parameter type ' - '`${withoutNullability(paramType.getDisplayString())}` is not supported.', + '`${withoutNullability(paramType.getDisplayString())}` is not supported. ' + 'Consider using @TypedQueryParameter with a custom encoder and decoder.', element: element, ); } @@ -145,6 +166,11 @@ String encodeField( PropertyAccessorElement element, List? metadata, ) { + final String? annotatedEncoder = element.encoder; + if (annotatedEncoder != null) { + return '$annotatedEncoder($selfFieldName.${element.displayName})'; + } + for (final _TypeHelper helper in _helpers) { if (helper._matchesType(element.returnType)) { String? encoder; @@ -177,7 +203,8 @@ String encodeField( } throw InvalidGenerationSourceError( - 'The return type `${element.returnType}` is not supported.', + 'The return type `${element.returnType}` is not supported. ' + 'Consider using @TypedQueryParameter with a custom encoder and decoder.', element: element, ); } @@ -201,11 +228,12 @@ T? getNodeDeclaration(InterfaceElement element) { /// Returns the comparison of a parameter with its default value. /// /// Otherwise, throws an [InvalidGenerationSourceError]. -String compareField( - FormalParameterElement param, - String value1, - String value2, -) { +String compareField(FormalParameterElement param) { + final String? annotatedCompare = param.compare; + if (annotatedCompare != null) { + return '$annotatedCompare($selfFieldName.${param.displayName}, ${param.defaultValueCode!})'; + } + for (final _TypeHelper helper in _helpers) { if (helper._matchesType(param.type)) { return helper._compare( @@ -216,7 +244,8 @@ String compareField( } throw InvalidGenerationSourceError( - 'The type `${param.type}` is not supported.', + 'The type `${param.type}` is not supported. ' + 'Consider using @TypedQueryParameter with a custom compare function.', element: param, ); } @@ -864,6 +893,61 @@ extension FormalParameterElementExtension on FormalParameterElement { displayName.kebab; return escapeDartString(name); } + + /// Returns the name of the decoder function for this parameter, if it has a + /// `TypedQueryParameter` annotation with a decoder specified. + String? get decoder { + final typedQueryParameterReader = ConstantReader( + _typedQueryParameterChecker.firstAnnotationOf(this), + ); + + return typedQueryParameterReader + .peek('decoder') + ?.objectValue + .toFunctionValue() + ?.qualifiedName; + } + + /// Returns the name of the encoder function for this parameter, if it has a + /// `TypedQueryParameter` annotation with an encoder specified. + String? get encoder { + final typedQueryParameterReader = ConstantReader( + _typedQueryParameterChecker.firstAnnotationOf(this), + ); + + return typedQueryParameterReader + .peek('encoder') + ?.objectValue + .toFunctionValue() + ?.qualifiedName; + } + + /// Returns the name of the compare function for this parameter, if it has a + /// `TypedQueryParameter` annotation with a compare function specified. + String? get compare { + final typedQueryParameterReader = ConstantReader( + _typedQueryParameterChecker.firstAnnotationOf(this), + ); + + return typedQueryParameterReader + .peek('compare') + ?.objectValue + .toFunctionValue() + ?.qualifiedName; + } +} + +/// Extension helpers on [PropertyAccessorElement]. +extension PropertyAccessorElementExtension on PropertyAccessorElement { + /// Returns the name of the encoder function for this property, if it has a + /// `TypedQueryParameter` annotation with an encoder specified. + String? get encoder { + return (enclosingElement as InterfaceElement) + .unnamedConstructor + ?.formalParameters + .firstWhereOrNull((parameter) => parameter.displayName == displayName) + ?.encoder; + } } /// An error thrown when a default value is used with a nullable type. @@ -880,3 +964,28 @@ class NullableDefaultValueError extends InvalidGenerationSourceError { const _typedQueryParameterChecker = TypeChecker.fromUrl( 'package:go_router/src/route_data.dart#TypedQueryParameter', ); + +/// Extension helpers on [ExecutableElement]. +extension ExecutableElementExtension on ExecutableElement { + /// Returns the name of `this` qualified with the class name if it's a + /// [MethodElement]. + String get qualifiedName { + if (this is TopLevelFunctionElement) { + return name!; + } + + if (this is MethodElement) { + return '${enclosingElement!.name}.$name'; + } + + if (this is ConstructorElement) { + // The default constructor. + if (name == 'new') { + return enclosingElement!.name!; + } + return '${enclosingElement!.name}.$name'; + } + + throw UnsupportedError('Not sure how to support typeof $runtimeType'); + } +} From 85990340c54bb7eb0143a0065ed5a1f54bbcec29 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 19 Feb 2026 13:58:22 +0800 Subject: [PATCH 02/11] Update documentation --- packages/go_router_builder/CHANGELOG.md | 4 ++++ packages/go_router_builder/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 498ba7174dcb..b2767ab1bc31 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.3.0 + +- Adds support for custom types through `TypedQueryParameter` annotation. The `encoder`, `decoder` and `compare` parameters allow specifying custom functions for encoding, decoding and comparing query parameters in `TypedGoRoute` constructors. For example, you can use a `DateTime` parameter with a custom encoder and decoder to convert it to and from a string representation in the URL. + ## 4.2.1 * Adds support for analyzer 11 and 12. diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 536b9c94b813..b5a13c602e7e 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 4.2.1 +version: 4.3.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 @@ -27,7 +27,7 @@ dev_dependencies: dart_style: ">=2.3.7 <4.0.0" flutter: sdk: flutter - go_router: ^17.1.0 + go_router: ^17.2.0 leak_tracker_flutter_testing: ">=3.0.0" package_config: ^2.1.1 pub_semver: ^2.1.5 From 0aabb5ee2e2b8e137a532cd1123a66e5d5dba998 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 19 Feb 2026 13:58:35 +0800 Subject: [PATCH 03/11] Update example --- .../lib/typed_query_parameter_example.dart | 103 +++++++++++++++++- .../lib/typed_query_parameter_example.g.dart | 17 +++ .../go_router_builder/example/pubspec.yaml | 2 +- .../test/typed_query_parameter_test.dart | 19 ++++ 4 files changed, 136 insertions(+), 5 deletions(-) diff --git a/packages/go_router_builder/example/lib/typed_query_parameter_example.dart b/packages/go_router_builder/example/lib/typed_query_parameter_example.dart index 6379ef424e98..2366d3d2307d 100644 --- a/packages/go_router_builder/example/lib/typed_query_parameter_example.dart +++ b/packages/go_router_builder/example/lib/typed_query_parameter_example.dart @@ -11,6 +11,35 @@ part 'typed_query_parameter_example.g.dart'; void main() => runApp(App()); +class CustomParameter { + const CustomParameter({required this.valueString, required this.valueInt}); + + final String valueString; + final int valueInt; + + static String? encode(CustomParameter? parameter) { + if (parameter == null) { + return null; + } + return '${parameter.valueString},${parameter.valueInt}'; + } + + static CustomParameter? decode(String? value) { + if (value == null) { + return null; + } + final List parts = value.split(','); + return CustomParameter( + valueString: parts[0], + valueInt: int.parse(parts[1]), + ); + } + + static bool compare(CustomParameter a, CustomParameter b) { + return a.valueString != b.valueString || a.valueInt != b.valueInt; + } +} + class App extends StatelessWidget { App({super.key}); @@ -27,34 +56,56 @@ class App extends StatelessWidget { @TypedGoRoute(path: '/int-route') class IntRoute extends GoRouteData with $IntRoute { IntRoute({ - @TypedQueryParameter(name: 'intField') this.intField, - @TypedQueryParameter(name: 'int_field_with_default_value') + @TypedQueryParameter(name: 'intField') this.intField, + @TypedQueryParameter(name: 'int_field_with_default_value') this.intFieldWithDefaultValue = 1, - @TypedQueryParameter(name: 'int field') this.intFieldWithSpace, + @TypedQueryParameter(name: 'int field') this.intFieldWithSpace, + @TypedQueryParameter( + encoder: CustomParameter.encode, + decoder: CustomParameter.decode, + ) + this.customField, + @TypedQueryParameter( + encoder: CustomParameter.encode, + decoder: CustomParameter.decode, + compare: CustomParameter.compare, + ) + this.customFieldWithDefaultValue = const CustomParameter( + valueString: 'default', + valueInt: 0, + ), }); final int? intField; final int intFieldWithDefaultValue; final int? intFieldWithSpace; + final CustomParameter? customField; + final CustomParameter customFieldWithDefaultValue; @override Widget build(BuildContext context, GoRouterState state) => Screen( intField: intField, intFieldWithDefaultValue: intFieldWithDefaultValue, intFieldWithSpace: intFieldWithSpace, + customField: customField, + customFieldWithDefaultValue: customFieldWithDefaultValue, ); } class Screen extends StatelessWidget { const Screen({ + super.key, required this.intField, required this.intFieldWithDefaultValue, this.intFieldWithSpace, - super.key, + this.customField, + required this.customFieldWithDefaultValue, }); final int? intField; final int intFieldWithDefaultValue; final int? intFieldWithSpace; + final CustomParameter? customField; + final CustomParameter customFieldWithDefaultValue; @override Widget build(BuildContext context) => Scaffold( @@ -75,6 +126,8 @@ class Screen extends StatelessWidget { intField: newValue, intFieldWithDefaultValue: intFieldWithDefaultValue, intFieldWithSpace: intFieldWithSpace, + customField: customField, + customFieldWithDefaultValue: customFieldWithDefaultValue, ).go(context); }, ), @@ -88,6 +141,8 @@ class Screen extends StatelessWidget { intField: intField, intFieldWithDefaultValue: newValue, intFieldWithSpace: intFieldWithSpace, + customField: customField, + customFieldWithDefaultValue: customFieldWithDefaultValue, ).go(context); }, ), @@ -101,6 +156,46 @@ class Screen extends StatelessWidget { intField: intField, intFieldWithDefaultValue: intFieldWithDefaultValue, intFieldWithSpace: newValue, + customField: customField, + customFieldWithDefaultValue: customFieldWithDefaultValue, + ).go(context); + }, + ), + ListTile( + title: const Text('customField:'), + subtitle: Text(CustomParameter.encode(customField) ?? ''), + trailing: const Icon(Icons.add), + onTap: () { + final newValue = CustomParameter( + valueString: '${customField?.valueString ?? ''}-', + valueInt: (customField?.valueInt ?? 0) + 1, + ); + IntRoute( + intField: intField, + intFieldWithDefaultValue: intFieldWithDefaultValue, + intFieldWithSpace: intFieldWithSpace, + customField: newValue, + customFieldWithDefaultValue: customFieldWithDefaultValue, + ).go(context); + }, + ), + ListTile( + title: const Text('customFieldWithDefaultValue:'), + subtitle: Text( + CustomParameter.encode(customFieldWithDefaultValue)!, + ), + trailing: const Icon(Icons.add), + onTap: () { + final newValue = CustomParameter( + valueString: '${customFieldWithDefaultValue.valueString}-', + valueInt: customFieldWithDefaultValue.valueInt + 1, + ); + IntRoute( + intField: intField, + intFieldWithDefaultValue: intFieldWithDefaultValue, + intFieldWithSpace: intFieldWithSpace, + customField: customField, + customFieldWithDefaultValue: newValue, ).go(context); }, ), diff --git a/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart b/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart index 16eb48104fc8..89af1087ba58 100644 --- a/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart +++ b/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart @@ -32,6 +32,14 @@ mixin $IntRoute on GoRouteData { state.uri.queryParameters, int.tryParse, ), + customField: CustomParameter.decode( + state.uri.queryParameters['custom-field'], + ), + customFieldWithDefaultValue: + CustomParameter.decode( + state.uri.queryParameters['custom-field-with-default-value'], + ) ?? + const CustomParameter(valueString: 'default', valueInt: 0), ); IntRoute get _self => this as IntRoute; @@ -46,6 +54,15 @@ mixin $IntRoute on GoRouteData { .toString(), if (_self.intFieldWithSpace != null) 'int field': _self.intFieldWithSpace!.toString(), + if (_self.customField != null) + 'custom-field': CustomParameter.encode(_self.customField), + if (CustomParameter.compare( + _self.customFieldWithDefaultValue, + const CustomParameter(valueString: 'default', valueInt: 0), + )) + 'custom-field-with-default-value': CustomParameter.encode( + _self.customFieldWithDefaultValue, + ), }, ); diff --git a/packages/go_router_builder/example/pubspec.yaml b/packages/go_router_builder/example/pubspec.yaml index e9bec7284d25..f46118e4098e 100644 --- a/packages/go_router_builder/example/pubspec.yaml +++ b/packages/go_router_builder/example/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: collection: ^1.15.0 flutter: sdk: flutter - go_router: ^17.1.0 + go_router: ^17.2.0 provider: 6.0.5 dev_dependencies: diff --git a/packages/go_router_builder/example/test/typed_query_parameter_test.dart b/packages/go_router_builder/example/test/typed_query_parameter_test.dart index b4236935ab99..564762b76200 100644 --- a/packages/go_router_builder/example/test/typed_query_parameter_test.dart +++ b/packages/go_router_builder/example/test/typed_query_parameter_test.dart @@ -40,4 +40,23 @@ void main() { ); expect(find.text('2'), findsOne); }); + + testWidgets('It should modify the custom fields when tapped', (tester) async { + await tester.pumpWidget(App()); + + expect(find.text('customField:'), findsOne); + expect(find.text('customFieldWithDefaultValue:'), findsOne); + + expect(find.text('default,0'), findsOne); + + await tester.tap(find.text('customField:')); + await tester.pumpAndSettle(); + expect(find.text('-,1'), findsOne); + expect(find.text('default,0'), findsOne); + + await tester.tap(find.text('customFieldWithDefaultValue:')); + await tester.pumpAndSettle(); + expect(find.text('-,1'), findsOne); + expect(find.text('default-,1'), findsOne); + }); } From 431e3ae034888f5f8db2d9dfe21b827f5316bd82 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 19 Feb 2026 13:58:43 +0800 Subject: [PATCH 04/11] Update tests --- .../test_inputs/typed_query_parameter.dart | 40 ++++++++++++++++++- .../typed_query_parameter.dart.expect | 17 ++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/go_router_builder/test_inputs/typed_query_parameter.dart b/packages/go_router_builder/test_inputs/typed_query_parameter.dart index f08e3f1b63be..fb8b19b5743e 100644 --- a/packages/go_router_builder/test_inputs/typed_query_parameter.dart +++ b/packages/go_router_builder/test_inputs/typed_query_parameter.dart @@ -6,13 +6,49 @@ import 'package:go_router/go_router.dart'; mixin $OverriddenParameterNameRoute {} +class CustomParameter { + const CustomParameter({required this.valueString, required this.valueInt}); + + final String valueString; + final int valueInt; + + static String? encode(CustomParameter? parameter) { + return ''; + } + + static CustomParameter? decode(String? value) { + return const CustomParameter(valueString: '', valueInt: 0); + } + + static bool compare(CustomParameter a, CustomParameter b) { + return true; + } +} + @TypedGoRoute(path: '/typed-go-route-parameter') class OverriddenParameterNameRoute extends GoRouteData with $OverriddenParameterNameRoute { OverriddenParameterNameRoute({ - @TypedQueryParameter(name: 'parameterNameOverride') this.withAnnotation, - @TypedQueryParameter(name: 'name with space') this.withSpace, + @TypedQueryParameter(name: 'parameterNameOverride') + this.withAnnotation, + @TypedQueryParameter(name: 'name with space') this.withSpace, + @TypedQueryParameter( + encoder: CustomParameter.encode, + decoder: CustomParameter.decode, + ) + this.customField, + @TypedQueryParameter( + encoder: CustomParameter.encode, + decoder: CustomParameter.decode, + compare: CustomParameter.compare, + ) + this.customFieldWithDefaultValue = const CustomParameter( + valueString: 'default', + valueInt: 0, + ), }); final int? withAnnotation; final String? withSpace; + final CustomParameter? customField; + final CustomParameter customFieldWithDefaultValue; } diff --git a/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect b/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect index b00208930f5f..408c08296c79 100644 --- a/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect +++ b/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect @@ -12,6 +12,14 @@ mixin $OverriddenParameterNameRoute on GoRouteData { int.tryParse, ), withSpace: state.uri.queryParameters['name with space'], + customField: CustomParameter.decode( + state.uri.queryParameters['custom-field'], + ), + customFieldWithDefaultValue: + CustomParameter.decode( + state.uri.queryParameters['custom-field-with-default-value'], + ) ?? + const CustomParameter(valueString: 'default', valueInt: 0), ); OverriddenParameterNameRoute get _self => @@ -24,6 +32,15 @@ mixin $OverriddenParameterNameRoute on GoRouteData { if (_self.withAnnotation != null) 'parameterNameOverride': _self.withAnnotation!.toString(), if (_self.withSpace != null) 'name with space': _self.withSpace, + if (_self.customField != null) + 'custom-field': CustomParameter.encode(_self.customField), + if (CustomParameter.compare( + _self.customFieldWithDefaultValue, + const CustomParameter(valueString: 'default', valueInt: 0), + )) + 'custom-field-with-default-value': CustomParameter.encode( + _self.customFieldWithDefaultValue, + ), }, ); From 17d53659a73ce6b5d11ae6b989143d4f5f7ac7bc Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Mon, 23 Feb 2026 15:33:04 +0700 Subject: [PATCH 05/11] Use loop for readability --- .../go_router_builder/lib/src/type_helpers.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index e05d069fa1ff..923f9c3ef57e 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -116,12 +116,15 @@ String decodeParameter( if (decoder != null) { decoded = ''; } else { - final ElementAnnotation? annotation = metadata?.firstWhereOrNull(( - ElementAnnotation annotation, - ) { - return annotation.computeConstantValue()?.type?.getDisplayString() == - 'CustomParameterCodec'; - }); + ElementAnnotation? annotation; + for (final ElementAnnotation element + in metadata ?? []) { + if (element.computeConstantValue()?.type?.getDisplayString() == + 'CustomParameterCodec') { + annotation = element; + break; + } + } if (annotation != null) { final String? decode = _getCustomCodec(annotation, 'decode'); final String? encode = _getCustomCodec(annotation, 'encode'); From a229e25622db7db8d14101a2da9c91b503697d42 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Mon, 23 Feb 2026 15:48:55 +0700 Subject: [PATCH 06/11] Clean code --- .../lib/src/type_helpers.dart | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index 923f9c3ef57e..df62c668cf31 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -110,44 +110,35 @@ String decodeParameter( } for (final _TypeHelper helper in _helpers) { - if (annotatedDecoder != null || helper._matchesType(paramType)) { - var decoder = annotatedDecoder; - String decoded; - if (decoder != null) { - decoded = ''; - } else { - ElementAnnotation? annotation; - for (final ElementAnnotation element - in metadata ?? []) { - if (element.computeConstantValue()?.type?.getDisplayString() == - 'CustomParameterCodec') { - annotation = element; - break; - } - } - if (annotation != null) { - final String? decode = _getCustomCodec(annotation, 'decode'); - final String? encode = _getCustomCodec(annotation, 'encode'); - if (decode != null && encode != null) { - decoder = decode; - } else { - throw InvalidGenerationSourceError( - 'The parameter type ' - '`${withoutNullability(paramType.getDisplayString())}` not have a well defined CustomParameterCodec decorator.', - element: element, - ); - } + if (helper._matchesType(paramType)) { + String? decoder; + final ElementAnnotation? annotation = metadata?.firstWhereOrNull(( + ElementAnnotation annotation, + ) { + return annotation.computeConstantValue()?.type?.getDisplayString() == + 'CustomParameterCodec'; + }); + if (annotation != null) { + final String? decode = _getCustomCodec(annotation, 'decode'); + final String? encode = _getCustomCodec(annotation, 'encode'); + if (decode != null && encode != null) { + decoder = decode; + } else { + throw InvalidGenerationSourceError( + 'The parameter type ' + '`${withoutNullability(paramType.getDisplayString())}` not have a well defined CustomParameterCodec decorator.', + element: element, + ); } - decoded = helper._decode(element, pathParameters, decoder); } - + String decoded = helper._decode(element, pathParameters, decoder); if (element.isOptional && element.hasDefaultValue) { if (element.type.isNullableType) { throw NullableDefaultValueError(element); } decoded += ' ?? ${element.defaultValueCode!}'; } - if (helper is _TypeHelperString && decoder != null) { + if (helper is _TypeHelperString) { return _fieldWithEncoder(decoded, decoder); } return decoded; @@ -983,7 +974,7 @@ extension ExecutableElementExtension on ExecutableElement { if (this is ConstructorElement) { // The default constructor. - if (name == 'new') { + if ((name?.isEmpty ?? false) || name == 'new') { return enclosingElement!.name!; } return '${enclosingElement!.name}.$name'; From f193ca2925f8d75a427b0add9cb1f30ff2b10aeb Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Mon, 23 Feb 2026 15:51:59 +0700 Subject: [PATCH 07/11] Revert unwanted changes --- packages/go_router_builder/lib/src/type_helpers.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index df62c668cf31..54d7b07d73e8 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -112,6 +112,7 @@ String decodeParameter( for (final _TypeHelper helper in _helpers) { if (helper._matchesType(paramType)) { String? decoder; + final ElementAnnotation? annotation = metadata?.firstWhereOrNull(( ElementAnnotation annotation, ) { @@ -138,7 +139,7 @@ String decodeParameter( } decoded += ' ?? ${element.defaultValueCode!}'; } - if (helper is _TypeHelperString) { + if (helper is _TypeHelperString && decoder != null) { return _fieldWithEncoder(decoded, decoder); } return decoded; From 70bd76b7edd733c7fb2de91a4c2418b8d6673312 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Tue, 24 Feb 2026 13:58:10 +0800 Subject: [PATCH 08/11] Remove unnecessary types --- .../example/lib/typed_query_parameter_example.dart | 6 +++--- .../test_inputs/typed_query_parameter.dart | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/go_router_builder/example/lib/typed_query_parameter_example.dart b/packages/go_router_builder/example/lib/typed_query_parameter_example.dart index 2366d3d2307d..e780fd39b007 100644 --- a/packages/go_router_builder/example/lib/typed_query_parameter_example.dart +++ b/packages/go_router_builder/example/lib/typed_query_parameter_example.dart @@ -56,10 +56,10 @@ class App extends StatelessWidget { @TypedGoRoute(path: '/int-route') class IntRoute extends GoRouteData with $IntRoute { IntRoute({ - @TypedQueryParameter(name: 'intField') this.intField, - @TypedQueryParameter(name: 'int_field_with_default_value') + @TypedQueryParameter(name: 'intField') this.intField, + @TypedQueryParameter(name: 'int_field_with_default_value') this.intFieldWithDefaultValue = 1, - @TypedQueryParameter(name: 'int field') this.intFieldWithSpace, + @TypedQueryParameter(name: 'int field') this.intFieldWithSpace, @TypedQueryParameter( encoder: CustomParameter.encode, decoder: CustomParameter.decode, diff --git a/packages/go_router_builder/test_inputs/typed_query_parameter.dart b/packages/go_router_builder/test_inputs/typed_query_parameter.dart index fb8b19b5743e..e39c21c8d670 100644 --- a/packages/go_router_builder/test_inputs/typed_query_parameter.dart +++ b/packages/go_router_builder/test_inputs/typed_query_parameter.dart @@ -29,9 +29,8 @@ class CustomParameter { class OverriddenParameterNameRoute extends GoRouteData with $OverriddenParameterNameRoute { OverriddenParameterNameRoute({ - @TypedQueryParameter(name: 'parameterNameOverride') - this.withAnnotation, - @TypedQueryParameter(name: 'name with space') this.withSpace, + @TypedQueryParameter(name: 'parameterNameOverride') this.withAnnotation, + @TypedQueryParameter(name: 'name with space') this.withSpace, @TypedQueryParameter( encoder: CustomParameter.encode, decoder: CustomParameter.decode, From 5a4dc84771ffde2dd5ddccfa429fee3fe1dc830c Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Sat, 28 Feb 2026 17:44:08 +0800 Subject: [PATCH 09/11] Enforce non nullable --- .../lib/src/type_helpers.dart | 21 ++++++++++++++++--- .../test_inputs/typed_query_parameter.dart | 4 ++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index 54d7b07d73e8..39d8f0b00039 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -98,8 +98,16 @@ String decodeParameter( if (annotatedDecoder != null) { // If there is a custom decoder, use it directly. - var decoded = - '$annotatedDecoder(state.${_stateValueAccess(element, pathParameters)})'; + final stateValueAccess = + 'state.${_stateValueAccess(element, pathParameters)}'; + String decoded; + if (!element.type.isNullableType && !element.hasDefaultValue) { + decoded = '$annotatedDecoder($stateValueAccess)'; + } else { + decoded = + '($stateValueAccess == null ? null : $annotatedDecoder($stateValueAccess!))'; + } + if (element.isOptional && element.hasDefaultValue) { if (element.type.isNullableType) { throw NullableDefaultValueError(element); @@ -163,7 +171,14 @@ String encodeField( ) { final String? annotatedEncoder = element.encoder; if (annotatedEncoder != null) { - return '$annotatedEncoder($selfFieldName.${element.displayName})'; + final fieldAccess = '$selfFieldName.${element.displayName}'; + final bool isNullable = element.returnType.isNullableType; + final encoded = '$annotatedEncoder($fieldAccess${isNullable ? '!' : ''})'; + if (isNullable) { + return '$fieldAccess != null ? $encoded : null'; + } else { + return encoded; + } } for (final _TypeHelper helper in _helpers) { diff --git a/packages/go_router_builder/test_inputs/typed_query_parameter.dart b/packages/go_router_builder/test_inputs/typed_query_parameter.dart index e39c21c8d670..5aa56ad22803 100644 --- a/packages/go_router_builder/test_inputs/typed_query_parameter.dart +++ b/packages/go_router_builder/test_inputs/typed_query_parameter.dart @@ -12,11 +12,11 @@ class CustomParameter { final String valueString; final int valueInt; - static String? encode(CustomParameter? parameter) { + static String encode(CustomParameter parameter) { return ''; } - static CustomParameter? decode(String? value) { + static CustomParameter decode(String value) { return const CustomParameter(valueString: '', valueInt: 0); } From 68c88866359861c056d3101aa450c872f18e8268 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Sat, 28 Feb 2026 17:44:21 +0800 Subject: [PATCH 10/11] Update example --- .../lib/typed_query_parameter_example.dart | 18 ++++++------------ .../lib/typed_query_parameter_example.g.dart | 18 +++++++++++------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/go_router_builder/example/lib/typed_query_parameter_example.dart b/packages/go_router_builder/example/lib/typed_query_parameter_example.dart index e780fd39b007..23b615783542 100644 --- a/packages/go_router_builder/example/lib/typed_query_parameter_example.dart +++ b/packages/go_router_builder/example/lib/typed_query_parameter_example.dart @@ -17,17 +17,11 @@ class CustomParameter { final String valueString; final int valueInt; - static String? encode(CustomParameter? parameter) { - if (parameter == null) { - return null; - } + static String encode(CustomParameter parameter) { return '${parameter.valueString},${parameter.valueInt}'; } - static CustomParameter? decode(String? value) { - if (value == null) { - return null; - } + static CustomParameter decode(String value) { final List parts = value.split(','); return CustomParameter( valueString: parts[0], @@ -163,7 +157,9 @@ class Screen extends StatelessWidget { ), ListTile( title: const Text('customField:'), - subtitle: Text(CustomParameter.encode(customField) ?? ''), + subtitle: Text( + customField == null ? '' : CustomParameter.encode(customField!), + ), trailing: const Icon(Icons.add), onTap: () { final newValue = CustomParameter( @@ -181,9 +177,7 @@ class Screen extends StatelessWidget { ), ListTile( title: const Text('customFieldWithDefaultValue:'), - subtitle: Text( - CustomParameter.encode(customFieldWithDefaultValue)!, - ), + subtitle: Text(CustomParameter.encode(customFieldWithDefaultValue)), trailing: const Icon(Icons.add), onTap: () { final newValue = CustomParameter( diff --git a/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart b/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart index 89af1087ba58..d4c30ac00c85 100644 --- a/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart +++ b/packages/go_router_builder/example/lib/typed_query_parameter_example.g.dart @@ -32,13 +32,15 @@ mixin $IntRoute on GoRouteData { state.uri.queryParameters, int.tryParse, ), - customField: CustomParameter.decode( - state.uri.queryParameters['custom-field'], - ), + customField: (state.uri.queryParameters['custom-field'] == null + ? null + : CustomParameter.decode(state.uri.queryParameters['custom-field']!)), customFieldWithDefaultValue: - CustomParameter.decode( - state.uri.queryParameters['custom-field-with-default-value'], - ) ?? + (state.uri.queryParameters['custom-field-with-default-value'] == null + ? null + : CustomParameter.decode( + state.uri.queryParameters['custom-field-with-default-value']!, + )) ?? const CustomParameter(valueString: 'default', valueInt: 0), ); @@ -55,7 +57,9 @@ mixin $IntRoute on GoRouteData { if (_self.intFieldWithSpace != null) 'int field': _self.intFieldWithSpace!.toString(), if (_self.customField != null) - 'custom-field': CustomParameter.encode(_self.customField), + 'custom-field': _self.customField != null + ? CustomParameter.encode(_self.customField!) + : null, if (CustomParameter.compare( _self.customFieldWithDefaultValue, const CustomParameter(valueString: 'default', valueInt: 0), From 25f16a44dd3ce8b19c4eb67f8b34a1fa2d7496cc Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Sat, 28 Feb 2026 17:53:24 +0800 Subject: [PATCH 11/11] Fix tests --- .../typed_query_parameter.dart.expect | 41 +++++++++++-------- .../test_inputs/unsupported_type.dart.expect | 2 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect b/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect index 408c08296c79..2011cff037e0 100644 --- a/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect +++ b/packages/go_router_builder/test_inputs/typed_query_parameter.dart.expect @@ -4,23 +4,26 @@ RouteBase get $overriddenParameterNameRoute => GoRouteData.$route( ); mixin $OverriddenParameterNameRoute on GoRouteData { - static OverriddenParameterNameRoute _fromState(GoRouterState state) => - OverriddenParameterNameRoute( - withAnnotation: _$convertMapValue( - 'parameterNameOverride', - state.uri.queryParameters, - int.tryParse, - ), - withSpace: state.uri.queryParameters['name with space'], - customField: CustomParameter.decode( - state.uri.queryParameters['custom-field'], - ), - customFieldWithDefaultValue: - CustomParameter.decode( - state.uri.queryParameters['custom-field-with-default-value'], - ) ?? - const CustomParameter(valueString: 'default', valueInt: 0), - ); + static OverriddenParameterNameRoute _fromState( + GoRouterState state, + ) => OverriddenParameterNameRoute( + withAnnotation: _$convertMapValue( + 'parameterNameOverride', + state.uri.queryParameters, + int.tryParse, + ), + withSpace: state.uri.queryParameters['name with space'], + customField: (state.uri.queryParameters['custom-field'] == null + ? null + : CustomParameter.decode(state.uri.queryParameters['custom-field']!)), + customFieldWithDefaultValue: + (state.uri.queryParameters['custom-field-with-default-value'] == null + ? null + : CustomParameter.decode( + state.uri.queryParameters['custom-field-with-default-value']!, + )) ?? + const CustomParameter(valueString: 'default', valueInt: 0), + ); OverriddenParameterNameRoute get _self => this as OverriddenParameterNameRoute; @@ -33,7 +36,9 @@ mixin $OverriddenParameterNameRoute on GoRouteData { 'parameterNameOverride': _self.withAnnotation!.toString(), if (_self.withSpace != null) 'name with space': _self.withSpace, if (_self.customField != null) - 'custom-field': CustomParameter.encode(_self.customField), + 'custom-field': _self.customField != null + ? CustomParameter.encode(_self.customField!) + : null, if (CustomParameter.compare( _self.customFieldWithDefaultValue, const CustomParameter(valueString: 'default', valueInt: 0), diff --git a/packages/go_router_builder/test_inputs/unsupported_type.dart.expect b/packages/go_router_builder/test_inputs/unsupported_type.dart.expect index c53b98f0b5a2..c1cbceeb66c6 100644 --- a/packages/go_router_builder/test_inputs/unsupported_type.dart.expect +++ b/packages/go_router_builder/test_inputs/unsupported_type.dart.expect @@ -1 +1 @@ -The parameter type `Stopwatch` is not supported. +The parameter type `Stopwatch` is not supported. Consider using @TypedQueryParameter with a custom encoder and decoder.