diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 3fcea0b2dc6..da0e1f2cd93 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.1 + +* Supports opt-in required extra parameters. [#117261](https://github.com/flutter/flutter/issues/117261) + ## 1.2.0 * Adds Support for ShellRoute diff --git a/packages/go_router_builder/example/lib/extra_example.dart b/packages/go_router_builder/example/lib/extra_example.dart new file mode 100644 index 00000000000..b16d6ff030c --- /dev/null +++ b/packages/go_router_builder/example/lib/extra_example.dart @@ -0,0 +1,123 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +part 'extra_example.g.dart'; + +void main() => runApp(const App()); + +final GoRouter _router = GoRouter( + routes: $appRoutes, + initialLocation: '/splash', +); + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +class Extra { + const Extra(this.value); + + final int value; +} + +@TypedGoRoute(path: '/requiredExtra') +class RequiredExtraRoute extends GoRouteData { + const RequiredExtraRoute({required this.$extra}); + + final Extra $extra; + + @override + Widget build(BuildContext context, GoRouterState state) => + RequiredExtraScreen(extra: $extra); +} + +class RequiredExtraScreen extends StatelessWidget { + const RequiredExtraScreen({super.key, required this.extra}); + + final Extra extra; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Required Extra')), + body: Center(child: Text('Extra: ${extra.value}')), + ); + } +} + +@TypedGoRoute(path: '/optionalExtra') +class OptionalExtraRoute extends GoRouteData { + const OptionalExtraRoute({this.$extra}); + + final Extra? $extra; + + @override + Widget build(BuildContext context, GoRouterState state) => + OptionalExtraScreen(extra: $extra); +} + +class OptionalExtraScreen extends StatelessWidget { + const OptionalExtraScreen({super.key, this.extra}); + + final Extra? extra; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Optional Extra')), + body: Center(child: Text('Extra: ${extra?.value}')), + ); + } +} + +@TypedGoRoute(path: '/splash') +class SplashRoute extends GoRouteData { + const SplashRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const Splash(); +} + +class Splash extends StatelessWidget { + const Splash({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Splash')), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Placeholder(), + ElevatedButton( + onPressed: () => + const RequiredExtraRoute($extra: Extra(1)).go(context), + child: const Text('Required Extra'), + ), + ElevatedButton( + onPressed: () => + const OptionalExtraRoute($extra: Extra(2)).go(context), + child: const Text('Optional Extra'), + ), + ElevatedButton( + onPressed: () => const OptionalExtraRoute().go(context), + child: const Text('Optional Extra (null)'), + ), + ], + ), + ); + } +} diff --git a/packages/go_router_builder/example/lib/extra_example.g.dart b/packages/go_router_builder/example/lib/extra_example.g.dart new file mode 100644 index 00000000000..b9febb44e7a --- /dev/null +++ b/packages/go_router_builder/example/lib/extra_example.g.dart @@ -0,0 +1,81 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'extra_example.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $requiredExtraRoute, + $optionalExtraRoute, + $splashRoute, + ]; + +RouteBase get $requiredExtraRoute => GoRouteData.$route( + path: '/requiredExtra', + factory: $RequiredExtraRouteExtension._fromState, + ); + +extension $RequiredExtraRouteExtension on RequiredExtraRoute { + static RequiredExtraRoute _fromState(GoRouterState state) => + RequiredExtraRoute( + $extra: state.extra as Extra, + ); + + String get location => GoRouteData.$location( + '/requiredExtra', + ); + + void go(BuildContext context) => context.go(location, extra: $extra); + + void push(BuildContext context) => context.push(location, extra: $extra); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: $extra); +} + +RouteBase get $optionalExtraRoute => GoRouteData.$route( + path: '/optionalExtra', + factory: $OptionalExtraRouteExtension._fromState, + ); + +extension $OptionalExtraRouteExtension on OptionalExtraRoute { + static OptionalExtraRoute _fromState(GoRouterState state) => + OptionalExtraRoute( + $extra: state.extra as Extra?, + ); + + String get location => GoRouteData.$location( + '/optionalExtra', + ); + + void go(BuildContext context) => context.go(location, extra: $extra); + + void push(BuildContext context) => context.push(location, extra: $extra); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: $extra); +} + +RouteBase get $splashRoute => GoRouteData.$route( + path: '/splash', + factory: $SplashRouteExtension._fromState, + ); + +extension $SplashRouteExtension on SplashRoute { + static SplashRoute _fromState(GoRouterState state) => const SplashRoute(); + + String get location => GoRouteData.$location( + '/splash', + ); + + void go(BuildContext context) => context.go(location); + + void push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 9d7c1d97b16..c0d1ace5476 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -265,7 +265,10 @@ RouteBase get $_routeGetterName => ${_routeDefinition()}; String get _newFromState { final StringBuffer buffer = StringBuffer('=>'); - if (_ctor.isConst && _ctorParams.isEmpty && _ctorQueryParams.isEmpty) { + if (_ctor.isConst && + _ctorParams.isEmpty && + _ctorQueryParams.isEmpty && + _extraParam == null) { buffer.writeln('const '); } @@ -367,7 +370,7 @@ GoRouteData.\$route( ); } - if (!_pathParams.contains(element.name)) { + if (!_pathParams.contains(element.name) && !element.isExtraField) { throw InvalidGenerationSourceError( 'Missing param `${element.name}` in path.', element: element, @@ -438,13 +441,7 @@ GoRouteData.\$route( late final List _ctorParams = _ctor.parameters.where((ParameterElement element) { - if (element.isRequired) { - if (element.isExtraField) { - throw InvalidGenerationSourceError( - 'Parameters named `$extraFieldName` cannot be required.', - element: element, - ); - } + if (element.isRequired && !element.isExtraField) { return true; } return false; diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index 70242442111..9f51e1b1576 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -93,12 +93,12 @@ String encodeField(PropertyAccessorElement element) { String enumMapName(InterfaceType type) => '_\$${type.element.name}EnumMap'; String _stateValueAccess(ParameterElement element) { - if (element.isRequired) { - return 'params[${escapeDartString(element.name)}]!'; + if (element.isExtraField) { + return 'extra as ${element.type.getDisplayString(withNullability: element.isOptional)}'; } - if (element.isExtraField) { - return 'extra as ${element.type.getDisplayString(withNullability: true)}'; + if (element.isRequired) { + return 'params[${escapeDartString(element.name)}]!'; } if (element.isOptional) { diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 99d3ba34f5d..fe1af19562b 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.2.0 +version: 1.2.1 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 348687f2b76..e7d2b1440bc 100644 --- a/packages/go_router_builder/test/builder_test.dart +++ b/packages/go_router_builder/test/builder_test.dart @@ -24,8 +24,8 @@ Future main() async { const Set _expectedAnnotatedTests = { 'AppliedToWrongClassType', 'BadPathParam', - 'ExtraMustBeOptional', 'ExtraValueRoute', + 'RequiredExtraValueRoute', 'MissingPathParam', 'MissingPathValue', 'MissingTypeAnnotation', 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 2b843c1e8d7..4a8a095ac40 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 @@ -53,15 +53,6 @@ class NullableRequiredParam extends GoRouteData { final int? id; } -@ShouldThrow( - r'Parameters named `$extra` cannot be required.', -) -@TypedGoRoute(path: r'bob/:$extra') -class ExtraMustBeOptional extends GoRouteData { - ExtraMustBeOptional({required this.$extra}); - final int $extra; -} - @ShouldThrow( 'Missing param `id` in path.', ) @@ -204,6 +195,36 @@ class ExtraValueRoute extends GoRouteData { final int? $extra; } +@ShouldGenerate(r''' +RouteBase get $requiredExtraValueRoute => GoRouteData.$route( + path: '/default-value-route', + factory: $RequiredExtraValueRouteExtension._fromState, + ); + +extension $RequiredExtraValueRouteExtension on RequiredExtraValueRoute { + static RequiredExtraValueRoute _fromState(GoRouterState state) => + RequiredExtraValueRoute( + $extra: state.extra as int, + ); + + String get location => GoRouteData.$location( + '/default-value-route', + ); + + void go(BuildContext context) => context.go(location, extra: $extra); + + void push(BuildContext context) => context.push(location, extra: $extra); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: $extra); +} +''') +@TypedGoRoute(path: '/default-value-route') +class RequiredExtraValueRoute extends GoRouteData { + RequiredExtraValueRoute({required this.$extra}); + final int $extra; +} + @ShouldThrow( 'Default value used with a nullable type. Only non-nullable type can have a default value.', todo: 'Remove the default value or make the type non-nullable.',