diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 8fc9b5f2dfd..b1f15fba7d3 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.7 + +* Supports default values for `Set`, `List` and `Iterable` route parameters. + ## 1.1.6 * Generates the const enum map for enums used in `List`, `Set` and `Iterable`. diff --git a/packages/go_router_builder/example/lib/all_types.dart b/packages/go_router_builder/example/lib/all_types.dart index 8e1f5177843..ce86905afc0 100644 --- a/packages/go_router_builder/example/lib/all_types.dart +++ b/packages/go_router_builder/example/lib/all_types.dart @@ -25,6 +25,8 @@ part 'all_types.g.dart'; TypedGoRoute(path: 'string-route/:requiredStringField'), TypedGoRoute(path: 'uri-route/:requiredUriField'), TypedGoRoute(path: 'iterable-route'), + TypedGoRoute( + path: 'iterable-route-with-default-values'), ]) @immutable class AllTypesBaseRoute extends GoRouteData { @@ -337,9 +339,23 @@ class IterableRoute extends GoRouteData { final Set? enumOnlyInSetField; @override - Widget build(BuildContext context, GoRouterState state) => - const BasePage( + Widget build(BuildContext context, GoRouterState state) => IterablePage( dataTitle: 'IterableRoute', + intIterableField: intIterableField, + doubleIterableField: doubleIterableField, + stringIterableField: stringIterableField, + boolIterableField: boolIterableField, + enumIterableField: enumIterableField, + intListField: intListField, + doubleListField: doubleListField, + stringListField: stringListField, + boolListField: boolListField, + enumListField: enumListField, + intSetField: intSetField, + doubleSetField: doubleSetField, + stringSetField: stringSetField, + boolSetField: boolSetField, + enumSetField: enumSetField, ); Widget drawerTile(BuildContext context) => ListTile( @@ -349,6 +365,75 @@ class IterableRoute extends GoRouteData { ); } +class IterableRouteWithDefaultValues extends GoRouteData { + const IterableRouteWithDefaultValues({ + this.intIterableField = const [0], + this.doubleIterableField = const [0, 1, 2], + this.stringIterableField = const ['defaultValue'], + this.boolIterableField = const [false], + this.enumIterableField = const [ + SportDetails.tennis, + SportDetails.hockey, + ], + this.intListField = const [0], + this.doubleListField = const [1, 2, 3], + this.stringListField = const ['defaultValue0', 'defaultValue1'], + this.boolListField = const [true], + this.enumListField = const [SportDetails.football], + this.intSetField = const {0, 1}, + this.doubleSetField = const {}, + this.stringSetField = const {'defaultValue'}, + this.boolSetField = const {true, false}, + this.enumSetField = const {SportDetails.hockey}, + }); + + final Iterable intIterableField; + final List intListField; + final Set intSetField; + + final Iterable doubleIterableField; + final List doubleListField; + final Set doubleSetField; + + final Iterable stringIterableField; + final List stringListField; + final Set stringSetField; + + final Iterable boolIterableField; + final List boolListField; + final Set boolSetField; + + final Iterable enumIterableField; + final List enumListField; + final Set enumSetField; + + @override + Widget build(BuildContext context, GoRouterState state) => IterablePage( + dataTitle: 'IterableRouteWithDefaultValues', + intIterableField: intIterableField, + doubleIterableField: doubleIterableField, + stringIterableField: stringIterableField, + boolIterableField: boolIterableField, + enumIterableField: enumIterableField, + intListField: intListField, + doubleListField: doubleListField, + stringListField: stringListField, + boolListField: boolListField, + enumListField: enumListField, + intSetField: intSetField, + doubleSetField: doubleSetField, + stringSetField: stringSetField, + boolSetField: boolSetField, + enumSetField: enumSetField, + ); + + Widget drawerTile(BuildContext context) => ListTile( + title: const Text('IterableRouteWithDefaultValues'), + onTap: () => go(context), + selected: GoRouter.of(context).location == location, + ); +} + class BasePage extends StatelessWidget { const BasePage({ required this.dataTitle, @@ -437,6 +522,7 @@ class BasePage extends StatelessWidget { SportDetails.hockey, }, ).drawerTile(context), + const IterableRouteWithDefaultValues().drawerTile(context), ], )), body: Center( @@ -475,3 +561,71 @@ class AllTypesApp extends StatelessWidget { initialLocation: const AllTypesBaseRoute().location, ); } + +class IterablePage extends StatelessWidget { + const IterablePage({ + required this.dataTitle, + this.intIterableField, + this.doubleIterableField, + this.stringIterableField, + this.boolIterableField, + this.enumIterableField, + this.intListField, + this.doubleListField, + this.stringListField, + this.boolListField, + this.enumListField, + this.intSetField, + this.doubleSetField, + this.stringSetField, + this.boolSetField, + this.enumSetField, + super.key, + }); + + final String dataTitle; + + final Iterable? intIterableField; + final List? intListField; + final Set? intSetField; + + final Iterable? doubleIterableField; + final List? doubleListField; + final Set? doubleSetField; + + final Iterable? stringIterableField; + final List? stringListField; + final Set? stringSetField; + + final Iterable? boolIterableField; + final List? boolListField; + final Set? boolSetField; + + final Iterable? enumIterableField; + final List? enumListField; + final Set? enumSetField; + + @override + Widget build(BuildContext context) { + return BasePage( + dataTitle: dataTitle, + queryParamWithDefaultValue: ?>{ + 'intIterableField': intIterableField, + 'intListField': intListField, + 'intSetField': intSetField, + 'doubleIterableField': doubleIterableField, + 'doubleListField': doubleListField, + 'doubleSetField': doubleSetField, + 'stringIterableField': stringIterableField, + 'stringListField': stringListField, + 'stringSetField': stringSetField, + 'boolIterableField': boolIterableField, + 'boolListField': boolListField, + 'boolSetField': boolSetField, + 'enumIterableField': enumIterableField, + 'enumListField': enumListField, + 'enumSetField': enumSetField, + }.toString(), + ); + } +} diff --git a/packages/go_router_builder/example/lib/all_types.g.dart b/packages/go_router_builder/example/lib/all_types.g.dart index c30ebeee286..f38dadc2021 100644 --- a/packages/go_router_builder/example/lib/all_types.g.dart +++ b/packages/go_router_builder/example/lib/all_types.g.dart @@ -64,6 +64,10 @@ GoRoute get $allTypesBaseRoute => GoRouteData.$route( path: 'iterable-route', factory: $IterableRouteExtension._fromState, ), + GoRouteData.$route( + path: 'iterable-route-with-default-values', + factory: $IterableRouteWithDefaultValuesExtension._fromState, + ), ], ); @@ -468,6 +472,120 @@ extension $IterableRouteExtension on IterableRoute { context.pushReplacement(location); } +extension $IterableRouteWithDefaultValuesExtension + on IterableRouteWithDefaultValues { + static IterableRouteWithDefaultValues _fromState(GoRouterState state) => + IterableRouteWithDefaultValues( + intIterableField: + state.queryParametersAll['int-iterable-field']?.map(int.parse) ?? + const [0], + doubleIterableField: state.queryParametersAll['double-iterable-field'] + ?.map(double.parse) ?? + const [0, 1, 2], + stringIterableField: + state.queryParametersAll['string-iterable-field']?.map((e) => e) ?? + const ['defaultValue'], + boolIterableField: state.queryParametersAll['bool-iterable-field'] + ?.map(_$boolConverter) ?? + const [false], + enumIterableField: state.queryParametersAll['enum-iterable-field'] + ?.map(_$SportDetailsEnumMap._$fromName) ?? + const [SportDetails.tennis, SportDetails.hockey], + intListField: state.queryParametersAll['int-list-field'] + ?.map(int.parse) + .toList() ?? + const [0], + doubleListField: state.queryParametersAll['double-list-field'] + ?.map(double.parse) + .toList() ?? + const [1, 2, 3], + stringListField: state.queryParametersAll['string-list-field'] + ?.map((e) => e) + .toList() ?? + const ['defaultValue0', 'defaultValue1'], + boolListField: state.queryParametersAll['bool-list-field'] + ?.map(_$boolConverter) + .toList() ?? + const [true], + enumListField: state.queryParametersAll['enum-list-field'] + ?.map(_$SportDetailsEnumMap._$fromName) + .toList() ?? + const [SportDetails.football], + intSetField: + state.queryParametersAll['int-set-field']?.map(int.parse).toSet() ?? + const {0, 1}, + doubleSetField: state.queryParametersAll['double-set-field'] + ?.map(double.parse) + .toSet() ?? + const {}, + stringSetField: state.queryParametersAll['string-set-field'] + ?.map((e) => e) + .toSet() ?? + const {'defaultValue'}, + boolSetField: state.queryParametersAll['bool-set-field'] + ?.map(_$boolConverter) + .toSet() ?? + const {true, false}, + enumSetField: state.queryParametersAll['enum-set-field'] + ?.map(_$SportDetailsEnumMap._$fromName) + .toSet() ?? + const {SportDetails.hockey}, + ); + + String get location => GoRouteData.$location( + '/iterable-route-with-default-values', + queryParams: { + if (intIterableField != const [0]) + 'int-iterable-field': + intIterableField.map((e) => e.toString()).toList(), + if (doubleIterableField != const [0, 1, 2]) + 'double-iterable-field': + doubleIterableField.map((e) => e.toString()).toList(), + if (stringIterableField != const ['defaultValue']) + 'string-iterable-field': stringIterableField.map((e) => e).toList(), + if (boolIterableField != const [false]) + 'bool-iterable-field': + boolIterableField.map((e) => e.toString()).toList(), + if (enumIterableField != + const [SportDetails.tennis, SportDetails.hockey]) + 'enum-iterable-field': + enumIterableField.map((e) => _$SportDetailsEnumMap[e]).toList(), + if (intListField != const [0]) + 'int-list-field': intListField.map((e) => e.toString()).toList(), + if (doubleListField != const [1, 2, 3]) + 'double-list-field': + doubleListField.map((e) => e.toString()).toList(), + if (stringListField != + const ['defaultValue0', 'defaultValue1']) + 'string-list-field': stringListField.map((e) => e).toList(), + if (boolListField != const [true]) + 'bool-list-field': boolListField.map((e) => e.toString()).toList(), + if (enumListField != const [SportDetails.football]) + 'enum-list-field': + enumListField.map((e) => _$SportDetailsEnumMap[e]).toList(), + if (intSetField != const {0, 1}) + 'int-set-field': intSetField.map((e) => e.toString()).toList(), + if (doubleSetField != const {}) + 'double-set-field': + doubleSetField.map((e) => e.toString()).toList(), + if (stringSetField != const {'defaultValue'}) + 'string-set-field': stringSetField.map((e) => e).toList(), + if (boolSetField != const {true, false}) + 'bool-set-field': boolSetField.map((e) => e.toString()).toList(), + if (enumSetField != const {SportDetails.hockey}) + 'enum-set-field': + enumSetField.map((e) => _$SportDetailsEnumMap[e]).toList(), + }, + ); + + void go(BuildContext context) => context.go(location); + + void push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} + const _$PersonDetailsEnumMap = { PersonDetails.hobbies: 'hobbies', PersonDetails.favoriteFood: 'favorite-food', diff --git a/packages/go_router_builder/example/pubspec.yaml b/packages/go_router_builder/example/pubspec.yaml index 0b7ffcbff9d..52dd2c40517 100644 --- a/packages/go_router_builder/example/pubspec.yaml +++ b/packages/go_router_builder/example/pubspec.yaml @@ -8,7 +8,7 @@ environment: dependencies: flutter: sdk: flutter - go_router: ^6.0.0 + go_router: ^6.2.0 provider: ^6.0.0 dev_dependencies: diff --git a/packages/go_router_builder/example/test/all_types_test.dart b/packages/go_router_builder/example/test/all_types_test.dart index 4b0cb3fb979..ec25b3df468 100644 --- a/packages/go_router_builder/example/test/all_types_test.dart +++ b/packages/go_router_builder/example/test/all_types_test.dart @@ -141,4 +141,84 @@ void main() { '/iterable-route?enum-iterable-field=football&int-list-field=1&int-list-field=2&int-list-field=3&enum-only-in-set-field=burger&enum-only-in-set-field=pizza'), findsOneWidget); }); + + testWidgets( + 'It should navigate to the iterable route with its default values', + (WidgetTester tester) async { + await tester.pumpWidget(AllTypesApp()); + + final ScaffoldState scaffoldState = + tester.firstState(find.byType(Scaffold)); + + const IterableRouteWithDefaultValues().go(scaffoldState.context); + await tester.pumpAndSettle(); + expect(find.text('IterableRouteWithDefaultValues'), findsOneWidget); + final IterablePage page = + tester.widget(find.byType(IterablePage)); + expect( + page, + isA().having( + (IterablePage page) => page.intIterableField, + 'intIterableField', + const [0], + ).having( + (IterablePage page) => page.intListField, + 'intListField', + const [0], + ).having( + (IterablePage page) => page.intSetField, + 'intSetField', + const {0, 1}, + ).having( + (IterablePage page) => page.doubleIterableField, + 'doubleIterableField', + const [0, 1, 2], + ).having( + (IterablePage page) => page.doubleListField, + 'doubleListField', + const [1, 2, 3], + ).having( + (IterablePage page) => page.doubleSetField, + 'doubleSetField', + const {}, + ).having( + (IterablePage page) => page.stringIterableField, + 'stringIterableField', + const ['defaultValue'], + ).having( + (IterablePage page) => page.stringListField, + 'stringListField', + const ['defaultValue0', 'defaultValue1'], + ).having( + (IterablePage page) => page.stringSetField, + 'stringSetField', + const {'defaultValue'}, + ).having( + (IterablePage page) => page.boolIterableField, + 'boolIterableField', + const [false], + ).having( + (IterablePage page) => page.boolListField, + 'boolListField', + const [true], + ).having( + (IterablePage page) => page.boolSetField, + 'boolSetField', + const {true, false}, + ).having( + (IterablePage page) => page.enumIterableField, + 'enumIterableField', + const [SportDetails.tennis, SportDetails.hockey], + ).having( + (IterablePage page) => page.enumListField, + 'enumListField', + const [SportDetails.football], + ).having( + (IterablePage page) => page.enumSetField, + 'enumSetField', + const {SportDetails.hockey}, + ), + ); + expect(find.text('/iterable-route-with-default-values'), findsOneWidget); + }); } diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index a0adb2ee595..70242442111 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -52,7 +52,14 @@ String decodeParameter(ParameterElement element) { final DartType paramType = element.type; for (final _TypeHelper helper in _helpers) { if (helper._matchesType(paramType)) { - return helper._decode(element); + String decoded = helper._decode(element); + if (element.isOptional && element.hasDefaultValue) { + if (element.type.isNullableType) { + throw NullableDefaultValueError(element); + } + decoded += ' ?? ${element.defaultValueCode!}'; + } + return decoded; } } @@ -95,14 +102,7 @@ String _stateValueAccess(ParameterElement element) { } if (element.isOptional) { - String value = 'queryParams[${escapeDartString(element.name.kebab)}]'; - if (element.hasDefaultValue) { - if (element.type.isNullableType) { - throw NullableDefaultValueError(element); - } - value += ' ?? ${element.defaultValueCode!}'; - } - return value; + return 'queryParams[${escapeDartString(element.name.kebab)}]'; } throw InvalidGenerationSourceError( @@ -292,6 +292,7 @@ state.queryParametersAll[${escapeDartString(parameterElement.name.kebab)}]'''; @override String _encode(String fieldName, DartType type) { + final String nullAwareAccess = type.isNullableType ? '?' : ''; if (type is ParameterizedType) { final DartType iterableType = type.typeArguments.first; @@ -300,7 +301,7 @@ state.queryParametersAll[${escapeDartString(parameterElement.name.kebab)}]'''; for (final _TypeHelper helper in _helpers) { if (helper._matchesType(iterableType)) { entriesTypeEncoder = ''' -?.map((e) => ${helper._encode('e', iterableType)}).toList()'''; +$nullAwareAccess.map((e) => ${helper._encode('e', iterableType)}).toList()'''; } } return ''' @@ -308,7 +309,7 @@ $fieldName$entriesTypeEncoder'''; } return ''' -$fieldName?.map((e) => e.toString()).toList()'''; +$fieldName$nullAwareAccess.map((e) => e.toString()).toList()'''; } @override @@ -326,14 +327,10 @@ abstract class _TypeHelperWithHelper extends _TypeHelper { final DartType paramType = parameterElement.type; if (!parameterElement.isRequired) { - String decoded = '$convertMapValueHelperName(' + return '$convertMapValueHelperName(' '${escapeDartString(parameterElement.name.kebab)}, ' 'state.queryParams, ' '${helperName(paramType)})'; - if (parameterElement.hasDefaultValue) { - decoded += ' ?? ${parameterElement.defaultValueCode!}'; - } - return decoded; } return '${helperName(paramType)}' '(state.${_stateValueAccess(parameterElement)})'; diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 36493448882..6508e011c8e 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: 1.1.6 +version: 1.1.7 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 f9124ed4520..348687f2b76 100644 --- a/packages/go_router_builder/test/builder_test.dart +++ b/packages/go_router_builder/test/builder_test.dart @@ -35,5 +35,6 @@ const Set _expectedAnnotatedTests = { 'EnumParam', 'DefaultValueRoute', 'NullableDefaultValueRoute', - 'IterableWithEnumRoute' + 'IterableWithEnumRoute', + 'IterableDefaultValueRoute', }; 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 39e6ef688e8..9875d2e921e 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 @@ -267,3 +267,38 @@ enum EnumOnlyUsedInIterable { b, c, } + +@ShouldGenerate(r''' +GoRoute get $iterableDefaultValueRoute => GoRouteData.$route( + path: '/iterable-default-value-route', + factory: $IterableDefaultValueRouteExtension._fromState, + ); + +extension $IterableDefaultValueRouteExtension on IterableDefaultValueRoute { + static IterableDefaultValueRoute _fromState(GoRouterState state) => + IterableDefaultValueRoute( + param: + state.queryParametersAll['param']?.map(int.parse) ?? const [0], + ); + + String get location => GoRouteData.$location( + '/iterable-default-value-route', + queryParams: { + if (param != const [0]) + 'param': param.map((e) => e.toString()).toList(), + }, + ); + + void go(BuildContext context) => context.go(location); + + void push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} +''') +@TypedGoRoute(path: '/iterable-default-value-route') +class IterableDefaultValueRoute extends GoRouteData { + IterableDefaultValueRoute({this.param = const [0]}); + final Iterable param; +}