diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index c47f328b9be..344e81a2339 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.3.0 + +- Adds support for `TypedRelativeGoRoute`. + ## 3.2.1 - Changes generated whitespace for better compatibility with new Dart formatter. diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index 643a664b6fa..9eea7ccf055 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -8,12 +8,12 @@ To use `go_router_builder`, you need to have the following dependencies in ```yaml dependencies: # ...along with your other dependencies - go_router: ^16.0.0 + go_router: ^16.2.0 dev_dependencies: # ...along with your other dev-dependencies build_runner: ^2.6.0 - go_router_builder: ^3.1.0 + go_router_builder: ^3.3.0 ``` ### Source code @@ -455,6 +455,32 @@ class MyGoRouteData extends GoRouteData with _$MyGoRouteData { An example is available [here](https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/shell_route_with_keys_example.dart). +## Relative routes + +Relative routes allow reusing the same `RouteData` in different parts of the route tree. +Define a relative route by extending `RelativeGoRouteData`. + + +```dart +@TypedRelativeGoRoute(path: 'details') +class DetailsRoute extends RelativeGoRouteData with _$DetailsRoute { + const DetailsRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => + const DetailsScreen(); +} +``` + +Navigate using the `goRelative` or `pushRelative` methods provided by the code generator: + + +```dart +void onTapRelative() => const DetailsRoute().goRelative(context); +``` + +Relative routing methods are not idempotent and will cause an error when the relative location does not match a route. + ## Run tests To run unit tests, run command `dart tool/run_tests.dart` from `packages/go_router_builder/`. diff --git a/packages/go_router_builder/example/lib/go_relative.dart b/packages/go_router_builder/example/lib/go_relative.dart new file mode 100644 index 00000000000..f9b39040fc4 --- /dev/null +++ b/packages/go_router_builder/example/lib/go_relative.dart @@ -0,0 +1,187 @@ +// 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, unreachable_from_main + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +part 'go_relative.g.dart'; + +void main() => runApp(const MyApp()); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router(routerConfig: _router); + } +} + +/// The route configuration. +final GoRouter _router = GoRouter(routes: $appRoutes); +const TypedRelativeGoRoute detailRoute = + TypedRelativeGoRoute( + path: 'details/:detailId', + routes: >[ + TypedRelativeGoRoute(path: 'settings/:settingId'), + ], + ); + +@TypedGoRoute( + path: '/', + routes: >[ + TypedGoRoute( + path: '/dashboard', + routes: >[detailRoute], + ), + detailRoute, + ], +) +class HomeRoute extends GoRouteData with _$HomeRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const HomeScreen(); + } +} + +class DashboardRoute extends GoRouteData with _$DashboardRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const DashboardScreen(); + } +} + +class DetailsRoute extends RelativeGoRouteData with _$DetailsRoute { + const DetailsRoute({required this.detailId}); + final String detailId; + + @override + Widget build(BuildContext context, GoRouterState state) { + return DetailsScreen(id: detailId); + } +} + +class SettingsRoute extends RelativeGoRouteData with _$SettingsRoute { + const SettingsRoute({required this.settingId}); + final String settingId; + + @override + Widget build(BuildContext context, GoRouterState state) { + return SettingsScreen(id: settingId); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + const DetailsRoute(detailId: 'DetailsId').goRelative(context); + }, + child: const Text('Go to the Details screen'), + ), + ElevatedButton( + onPressed: () { + DashboardRoute().go(context); + }, + child: const Text('Go to the Dashboard screen'), + ), + ], + ), + ); + } +} + +/// The home screen +class DashboardScreen extends StatelessWidget { + /// Constructs a [DashboardScreen] + const DashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Dashboard Screen')), + body: Column( + children: [ + ElevatedButton( + onPressed: () { + const DetailsRoute(detailId: 'DetailsId').goRelative(context); + }, + child: const Text('Go to the Details screen'), + ), + ElevatedButton( + onPressed: () => context.pop(), + child: const Text('Go back'), + ), + ], + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key, required this.id}); + + final String id; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Details Screen $id')), + body: Center( + child: Column( + children: [ + ElevatedButton( + onPressed: () => context.pop(), + child: const Text('Go back'), + ), + ElevatedButton( + onPressed: + () => const SettingsRoute( + settingId: 'SettingsId', + ).goRelative(context), + child: const Text('Go to the Settings screen'), + ), + ], + ), + ), + ); + } +} + +/// The details screen +class SettingsScreen extends StatelessWidget { + /// Constructs a [SettingsScreen] + const SettingsScreen({super.key, required this.id}); + + final String id; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Settings Screen $id')), + body: Center( + child: TextButton( + onPressed: () => context.pop(), + child: const Text('Go back'), + ), + ), + ); + } +} diff --git a/packages/go_router_builder/example/lib/go_relative.g.dart b/packages/go_router_builder/example/lib/go_relative.g.dart new file mode 100644 index 00000000000..1554bf21add --- /dev/null +++ b/packages/go_router_builder/example/lib/go_relative.g.dart @@ -0,0 +1,148 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'go_relative.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [$homeRoute]; + +RouteBase get $homeRoute => GoRouteData.$route( + path: '/', + factory: _$HomeRoute._fromState, + routes: [ + GoRouteData.$route( + path: '/dashboard', + factory: _$DashboardRoute._fromState, + routes: [ + RelativeGoRouteData.$route( + path: 'details/:detailId', + + factory: _$DetailsRoute._fromState, + routes: [ + RelativeGoRouteData.$route( + path: 'settings/:settingId', + + factory: _$SettingsRoute._fromState, + ), + ], + ), + ], + ), + RelativeGoRouteData.$route( + path: 'details/:detailId', + + factory: _$DetailsRoute._fromState, + routes: [ + RelativeGoRouteData.$route( + path: 'settings/:settingId', + + factory: _$SettingsRoute._fromState, + ), + ], + ), + ], +); + +mixin _$HomeRoute on GoRouteData { + static HomeRoute _fromState(GoRouterState state) => HomeRoute(); + + @override + String get location => GoRouteData.$location('/'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin _$DashboardRoute on GoRouteData { + static DashboardRoute _fromState(GoRouterState state) => DashboardRoute(); + + @override + String get location => GoRouteData.$location('/dashboard'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin _$DetailsRoute on RelativeGoRouteData { + static DetailsRoute _fromState(GoRouterState state) => + DetailsRoute(detailId: state.pathParameters['detailId']!); + + DetailsRoute get _self => this as DetailsRoute; + + @override + String get subLocation => RelativeGoRouteData.$location( + 'details/${Uri.encodeComponent(_self.detailId)}', + ); + + @override + String get relativeLocation => './$subLocation'; + + @override + void goRelative(BuildContext context) => context.go(relativeLocation); + + @override + Future pushRelative(BuildContext context) => + context.push(relativeLocation); + + @override + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + @override + void replaceRelative(BuildContext context) => + context.replace(relativeLocation); +} + +mixin _$SettingsRoute on RelativeGoRouteData { + static SettingsRoute _fromState(GoRouterState state) => + SettingsRoute(settingId: state.pathParameters['settingId']!); + + SettingsRoute get _self => this as SettingsRoute; + + @override + String get subLocation => RelativeGoRouteData.$location( + 'settings/${Uri.encodeComponent(_self.settingId)}', + ); + + @override + String get relativeLocation => './$subLocation'; + + @override + void goRelative(BuildContext context) => context.go(relativeLocation); + + @override + Future pushRelative(BuildContext context) => + context.push(relativeLocation); + + @override + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + @override + void replaceRelative(BuildContext context) => + context.replace(relativeLocation); +} diff --git a/packages/go_router_builder/example/lib/readme_excerpts.dart b/packages/go_router_builder/example/lib/readme_excerpts.dart index e4a457497cd..0bc4660cf36 100644 --- a/packages/go_router_builder/example/lib/readme_excerpts.dart +++ b/packages/go_router_builder/example/lib/readme_excerpts.dart @@ -57,6 +57,10 @@ void otherDoc(BuildContext context) { } // #enddocregion tapWithExtra + // #docregion goRelative + void onTapRelative() => const DetailsRoute().goRelative(context); + // #enddocregion goRelative + final LoginInfo loginInfo = LoginInfo(); final GoRouter routerWithRedirect = GoRouter( @@ -396,3 +400,23 @@ class MyGoRouteData extends GoRouteData with _$MyGoRouteData { } // #enddocregion MyShellRouteData + +// #docregion relativeRoute +@TypedRelativeGoRoute(path: 'details') +class DetailsRoute extends RelativeGoRouteData with _$DetailsRoute { + const DetailsRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => + const DetailsScreen(); +} +// #enddocregion relativeRoute + +class DetailsScreen extends StatelessWidget { + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/packages/go_router_builder/example/lib/readme_excerpts.g.dart b/packages/go_router_builder/example/lib/readme_excerpts.g.dart index 1a643f748f8..878f8027153 100644 --- a/packages/go_router_builder/example/lib/readme_excerpts.g.dart +++ b/packages/go_router_builder/example/lib/readme_excerpts.g.dart @@ -18,6 +18,7 @@ List get $appRoutes => [ $myMaterialRouteWithKey, $fancyRoute, $myShellRouteData, + $detailsRoute, ]; RouteBase get $homeRoute => GoRouteData.$route( @@ -356,3 +357,34 @@ mixin _$MyGoRouteData on GoRouteData { @override void replace(BuildContext context) => context.replace(location); } + +RouteBase get $detailsRoute => RelativeGoRouteData.$route( + path: 'details', + + factory: _$DetailsRoute._fromState, +); + +mixin _$DetailsRoute on RelativeGoRouteData { + static DetailsRoute _fromState(GoRouterState state) => const DetailsRoute(); + + @override + String get subLocation => RelativeGoRouteData.$location('details'); + + @override + String get relativeLocation => './$subLocation'; + + @override + void goRelative(BuildContext context) => context.go(relativeLocation); + + @override + Future pushRelative(BuildContext context) => + context.push(relativeLocation); + + @override + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + @override + void replaceRelative(BuildContext context) => + context.replace(relativeLocation); +} diff --git a/packages/go_router_builder/example/pubspec.yaml b/packages/go_router_builder/example/pubspec.yaml index 5543b0695cf..a582818bdbf 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: ^16.0.0 + go_router: ^16.2.0 provider: 6.0.5 dev_dependencies: diff --git a/packages/go_router_builder/example/test/go_relative_test.dart b/packages/go_router_builder/example/test/go_relative_test.dart new file mode 100644 index 00000000000..3efd323e8c2 --- /dev/null +++ b/packages/go_router_builder/example/test/go_relative_test.dart @@ -0,0 +1,55 @@ +// 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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_builder_example/go_relative.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.byType(example.HomeScreen), findsOneWidget); + + // From Home screen, go to Details screen + await tester.tap(find.text('Go to the Details screen')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.text('Go to the Settings screen')); + await tester.pumpAndSettle(); + expect(find.byType(example.SettingsScreen), findsOneWidget); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(example.HomeScreen), findsOneWidget); + + await tester.tap(find.text('Go to the Dashboard screen')); + await tester.pumpAndSettle(); + expect(find.byType(example.DashboardScreen), findsOneWidget); + + // From Dashboard screen, go to Details screen + await tester.tap(find.text('Go to the Details screen')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.text('Go to the Settings screen')); + await tester.pumpAndSettle(); + expect(find.byType(example.SettingsScreen), findsOneWidget); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(example.DashboardScreen), findsOneWidget); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(example.HomeScreen), findsOneWidget); + }); +} diff --git a/packages/go_router_builder/lib/src/go_router_generator.dart b/packages/go_router_builder/lib/src/go_router_generator.dart index c18631bdc70..3e325a5647a 100644 --- a/packages/go_router_builder/lib/src/go_router_generator.dart +++ b/packages/go_router_builder/lib/src/go_router_generator.dart @@ -16,6 +16,7 @@ const String _routeDataUrl = 'package:go_router/src/route_data.dart'; const Map _annotations = { 'TypedGoRoute': 'GoRouteData', + 'TypedRelativeGoRoute': 'RelativeGoRouteData', 'TypedShellRoute': 'ShellRouteData', 'TypedStatefulShellBranch': 'StatefulShellBranchData', 'TypedStatefulShellRoute': 'StatefulShellRouteData', diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 7405028bcd9..2bad8d2c8a7 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -186,47 +186,14 @@ class StatefulShellBranchConfig extends RouteBaseConfig { String get dataConvertionFunctionName => r'$branch'; } -/// The configuration to generate class declarations for a GoRouteData. -class GoRouteConfig extends RouteBaseConfig { - GoRouteConfig._({ - required this.path, - required this.name, - required this.caseSensitive, - required this.parentNavigatorKey, - required super.routeDataClass, - required super.parent, - }) : super._(); - - /// The path of the GoRoute to be created by this configuration. - final String path; - - /// The name of the GoRoute to be created by this configuration. - final String? name; - - /// The case sensitivity of the GoRoute to be created by this configuration. - final bool caseSensitive; - - /// The parent navigator key. - final String? parentNavigatorKey; +/// A mixin that provides common functionality for GoRoute-based configurations. +mixin _GoRouteMixin on RouteBaseConfig { + String get _basePathForLocation; late final Set _pathParams = pathParametersFromPattern( - _rawJoinedPath, + _basePathForLocation, ); - String get _rawJoinedPath { - final List pathSegments = []; - - RouteBaseConfig? config = this; - while (config != null) { - if (config is GoRouteConfig) { - pathSegments.add(config.path); - } - config = config.parent; - } - - return p.url.joinAll(pathSegments.reversed); - } - // construct path bits using parent bits // if there are any queryParam objects, add in the `queryParam` bits String get _locationArgs { @@ -252,10 +219,13 @@ class GoRouteConfig extends RouteBaseConfig { return MapEntry(pathParameter, valueBuffer.toString()); }), ); - final String location = patternToPath(_rawJoinedPath, pathParameters); + final String location = patternToPath(_basePathForLocation, pathParameters); return "'$location'"; } + /// The definition of the mixin to be generated. + String get _mixinDefinition; + FormalParameterElement? get _extraParam => _ctor.formalParameters.singleWhereOrNull( (FormalParameterElement element) => element.isExtraField, @@ -403,6 +373,85 @@ class GoRouteConfig extends RouteBaseConfig { ..._enumDeclarations(), ]; + /// Returns code representing the constant maps that contain the `enum` to + /// [String] mapping for each referenced enum. + Iterable _enumDeclarations() { + final Set enumParamTypes = {}; + + for (final FormalParameterElement ctorParam in [ + ..._ctorParams, + ..._ctorQueryParams, + ]) { + DartType potentialEnumType = ctorParam.type; + if (potentialEnumType is ParameterizedType && + (ctorParam.type as ParameterizedType).typeArguments.isNotEmpty) { + potentialEnumType = + (ctorParam.type as ParameterizedType).typeArguments.first; + } + + if (potentialEnumType.isEnum) { + enumParamTypes.add(potentialEnumType as InterfaceType); + } + + // Support for enum extension types + final DartType representedType = potentialEnumType.extensionTypeErasure; + if (potentialEnumType != representedType && representedType.isEnum) { + enumParamTypes.add(representedType as InterfaceType); + } + } + return enumParamTypes.map(_enumMapConst); + } + + @override + String get factorConstructorParameters => 'factory: $_mixinName._fromState,'; + + @override + String get dataConvertionFunctionName => r'$route'; +} + +/// The configuration to generate class declarations for a GoRouteData. +class GoRouteConfig extends RouteBaseConfig with _GoRouteMixin { + GoRouteConfig._({ + required this.path, + required this.name, + required this.caseSensitive, + required this.parentNavigatorKey, + required super.routeDataClass, + required super.parent, + }) : super._(); + + /// The path of the GoRoute to be created by this configuration. + final String path; + + /// The name of the GoRoute to be created by this configuration. + final String? name; + + /// The case sensitivity of the GoRoute to be created by this configuration. + final bool caseSensitive; + + /// The parent navigator key. + final String? parentNavigatorKey; + + String get _rawJoinedPath { + final List pathSegments = []; + + RouteBaseConfig? config = this; + while (config != null) { + if (config + case GoRouteConfig(:final String path) || + RelativeGoRouteConfig(:final String path)) { + pathSegments.add(path); + } + config = config.parent; + } + + return p.url.joinAll(pathSegments.reversed); + } + + @override + String get _basePathForLocation => _rawJoinedPath; + + @override String get _mixinDefinition { final bool hasMixin = getNodeDeclaration(routeDataClass) @@ -419,7 +468,7 @@ class GoRouteConfig extends RouteBaseConfig { } return ''' -mixin $_mixinName on GoRouteData { +mixin $_mixinName on $routeDataClassName { static $_className _fromState(GoRouterState state) $_fromStateConstructor $_castedSelf @override @@ -444,50 +493,93 @@ mixin $_mixinName on GoRouteData { '''; } - /// Returns code representing the constant maps that contain the `enum` to - /// [String] mapping for each referenced enum. - Iterable _enumDeclarations() { - final Set enumParamTypes = {}; + @override + String get routeConstructorParameters => + 'path: ${escapeDartString(path)},' + '${name != null ? 'name: ${escapeDartString(name!)},' : ''}' + '${caseSensitive ? '' : 'caseSensitive: $caseSensitive,'}' + '${parentNavigatorKey == null ? '' : 'parentNavigatorKey: $parentNavigatorKey,'}'; - for (final FormalParameterElement ctorParam in [ - ..._ctorParams, - ..._ctorQueryParams, - ]) { - DartType potentialEnumType = ctorParam.type; - if (potentialEnumType is ParameterizedType && - (ctorParam.type as ParameterizedType).typeArguments.isNotEmpty) { - potentialEnumType = - (ctorParam.type as ParameterizedType).typeArguments.first; - } + @override + String get routeDataClassName => 'GoRouteData'; +} - if (potentialEnumType.isEnum) { - enumParamTypes.add(potentialEnumType as InterfaceType); - } +/// The configuration to generate class declarations for a RelativeGoRouteData. +class RelativeGoRouteConfig extends RouteBaseConfig with _GoRouteMixin { + RelativeGoRouteConfig._({ + required this.path, + required this.caseSensitive, + required this.parentNavigatorKey, + required super.routeDataClass, + required super.parent, + }) : super._(); - // Support for enum extension types - final DartType representedType = potentialEnumType.extensionTypeErasure; - if (potentialEnumType != representedType && representedType.isEnum) { - enumParamTypes.add(representedType as InterfaceType); - } + /// The path of the GoRoute to be created by this configuration. + final String path; + + /// The case sensitivity of the GoRoute to be created by this configuration. + final bool caseSensitive; + + /// The parent navigator key. + final String? parentNavigatorKey; + + @override + String get _basePathForLocation => path; + + @override + String get _mixinDefinition { + final bool hasMixin = + getNodeDeclaration(routeDataClass) + ?.withClause + ?.mixinTypes + .any((NamedType e) => e.name2.toString() == _mixinName) ?? + false; + + if (!hasMixin) { + throw InvalidGenerationSourceError( + 'Missing mixin clause `with $_mixinName`', + element: routeDataClass, + ); } - return enumParamTypes.map(_enumMapConst); - } + return ''' +mixin $_mixinName on $routeDataClassName { + static $_className _fromState(GoRouterState state) $_fromStateConstructor + $_castedSelf @override - String get factorConstructorParameters => 'factory: $_mixinName._fromState,'; + String get subLocation => RelativeGoRouteData.\$location($_locationArgs,$_locationQueryParams); + + @override + String get relativeLocation => './\$subLocation'; @override - String get routeConstructorParameters => - 'path: ${escapeDartString(path)},' - '${name != null ? 'name: ${escapeDartString(name!)},' : ''}' - '${caseSensitive ? '' : 'caseSensitive: $caseSensitive,'}' - '${parentNavigatorKey == null ? '' : 'parentNavigatorKey: $parentNavigatorKey,'}'; + void goRelative(BuildContext context) => + context.go(relativeLocation${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); @override - String get routeDataClassName => 'GoRouteData'; + Future pushRelative(BuildContext context) => + context.push(relativeLocation${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); + + @override + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); + + @override + void replaceRelative(BuildContext context) => + context.replace(relativeLocation${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); +} +'''; + } @override - String get dataConvertionFunctionName => r'$route'; + String get routeConstructorParameters => ''' + path: ${escapeDartString(path)}, + ${caseSensitive ? '' : 'caseSensitive: $caseSensitive,'} + ${parentNavigatorKey == null ? '' : 'parentNavigatorKey: $parentNavigatorKey,'} +'''; + + @override + String get routeDataClassName => 'RelativeGoRouteData'; } /// Represents a `TypedGoRoute` annotation to the builder. @@ -519,11 +611,23 @@ abstract class RouteBaseConfig { factory RouteBaseConfig._fromAnnotation( ConstantReader reader, InterfaceElement2 element, - RouteBaseConfig? parent, - ) { + RouteBaseConfig? parent, { + bool isAncestorRelative = false, + }) { assert(!reader.isNull, 'reader should not be null'); final InterfaceType type = reader.objectValue.type! as InterfaceType; final String typeName = type.element.name; + + if (isAncestorRelative && typeName == 'TypedGoRoute') { + throw InvalidGenerationSourceError( + 'TypedRelativeGoRoute cannot have a TypedGoRoute descendant.', + element: element, + ); + } + + final bool isRelative = + isAncestorRelative || typeName == 'TypedRelativeGoRoute'; + final DartType typeParamType = type.typeArguments.single; if (typeParamType is! InterfaceType) { throw InvalidGenerationSourceError( @@ -622,6 +726,32 @@ abstract class RouteBaseConfig { parameterName: r'$parentNavigatorKey', ), ); + case 'TypedRelativeGoRoute': + final ConstantReader pathValue = reader.read('path'); + if (pathValue.isNull) { + throw InvalidGenerationSourceError( + 'Missing `path` value on annotation.', + element: element, + ); + } + final String pathString = pathValue.stringValue; + if (pathString.startsWith('/')) { + throw InvalidGenerationSourceError( + 'The path for a TypedRelativeGoRoute cannot start with "/".', + element: element, + ); + } + final ConstantReader caseSensitiveValue = reader.read('caseSensitive'); + value = RelativeGoRouteConfig._( + path: pathValue.stringValue, + caseSensitive: caseSensitiveValue.boolValue, + routeDataClass: classElement, + parent: parent, + parentNavigatorKey: _generateParameterGetterCode( + classElement, + parameterName: r'$parentNavigatorKey', + ), + ); default: throw UnsupportedError('Unrecognized type $typeName'); } @@ -635,6 +765,7 @@ abstract class RouteBaseConfig { ConstantReader(e), element, value, + isAncestorRelative: isRelative, ), ), ); diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 7825f7f28b4..801facafad4 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: 3.2.1 +version: 3.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 @@ -26,7 +26,7 @@ dev_dependencies: dart_style: '>=2.3.7 <4.0.0' flutter: sdk: flutter - go_router: ^16.0.0 + go_router: ^16.2.0 leak_tracker_flutter_testing: ">=3.0.0" package_config: ^2.1.1 pub_semver: ^2.1.5 diff --git a/packages/go_router_builder/test_inputs/go_relative.dart b/packages/go_router_builder/test_inputs/go_relative.dart new file mode 100644 index 00000000000..955ceaaded3 --- /dev/null +++ b/packages/go_router_builder/test_inputs/go_relative.dart @@ -0,0 +1,42 @@ +// 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. + +import 'package:go_router/go_router.dart'; + +mixin _$Route1 {} +mixin _$Route2 {} +mixin _$RelativeRoute {} +mixin _$InnerRelativeRoute {} + +const TypedRelativeGoRoute relativeRoute = + TypedRelativeGoRoute( + path: 'relative-route', + routes: >[ + TypedRelativeGoRoute(path: 'inner-relative-route'), + ], + ); + +@TypedGoRoute( + path: 'route-1', + routes: >[relativeRoute], +) +class Route1 extends GoRouteData with _$Route1 { + const Route1(); +} + +@TypedGoRoute( + path: 'route-2', + routes: >[relativeRoute], +) +class Route2 extends GoRouteData with _$Route2 { + const Route2(); +} + +class RelativeRoute extends RelativeGoRouteData with _$RelativeRoute { + const RelativeRoute(); +} + +class InnerRelativeRoute extends RelativeGoRouteData with _$InnerRelativeRoute { + const InnerRelativeRoute(); +} diff --git a/packages/go_router_builder/test_inputs/go_relative.dart.expect b/packages/go_router_builder/test_inputs/go_relative.dart.expect new file mode 100644 index 00000000000..90742469050 --- /dev/null +++ b/packages/go_router_builder/test_inputs/go_relative.dart.expect @@ -0,0 +1,132 @@ +RouteBase get $route1 => GoRouteData.$route( + path: 'route-1', + factory: _$Route1._fromState, + routes: [ + RelativeGoRouteData.$route( + path: 'relative-route', + factory: _$RelativeRoute._fromState, + routes: [ + RelativeGoRouteData.$route( + path: 'inner-relative-route', + factory: _$InnerRelativeRoute._fromState, + ), + ], + ), + ], + ); + +mixin _$Route1 on GoRouteData { + static Route1 _fromState(GoRouterState state) => const Route1(); + + @override + String get location => GoRouteData.$location( + 'route-1', + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin _$RelativeRoute on RelativeGoRouteData { + static RelativeRoute _fromState(GoRouterState state) => const RelativeRoute(); + + @override + String get subLocation => RelativeGoRouteData.$location( + 'relative-route', + ); + + @override + String get relativeLocation => './$subLocation'; + + @override + void goRelative(BuildContext context) => context.go(relativeLocation); + + @override + Future pushRelative(BuildContext context) => + context.push(relativeLocation); + + @override + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + @override + void replaceRelative(BuildContext context) => + context.replace(relativeLocation); +} + +mixin _$InnerRelativeRoute on RelativeGoRouteData { + static InnerRelativeRoute _fromState(GoRouterState state) => + const InnerRelativeRoute(); + + @override + String get subLocation => RelativeGoRouteData.$location( + 'inner-relative-route', + ); + + @override + String get relativeLocation => './$subLocation'; + + @override + void goRelative(BuildContext context) => context.go(relativeLocation); + + @override + Future pushRelative(BuildContext context) => + context.push(relativeLocation); + + @override + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + @override + void replaceRelative(BuildContext context) => + context.replace(relativeLocation); +} + +RouteBase get $route2 => GoRouteData.$route( + path: 'route-2', + factory: _$Route2._fromState, + routes: [ + RelativeGoRouteData.$route( + path: 'relative-route', + factory: _$RelativeRoute._fromState, + routes: [ + RelativeGoRouteData.$route( + path: 'inner-relative-route', + factory: _$InnerRelativeRoute._fromState, + ), + ], + ), + ], + ); + +mixin _$Route2 on GoRouteData { + static Route2 _fromState(GoRouterState state) => const Route2(); + + @override + String get location => GoRouteData.$location( + 'route-2', + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} diff --git a/packages/go_router_builder/test_inputs/no_mixin_relative.dart b/packages/go_router_builder/test_inputs/no_mixin_relative.dart new file mode 100644 index 00000000000..64d17d17e4f --- /dev/null +++ b/packages/go_router_builder/test_inputs/no_mixin_relative.dart @@ -0,0 +1,8 @@ +// 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. + +import 'package:go_router/go_router.dart'; + +@TypedRelativeGoRoute(path: 'path') +class RelativeRoute extends RelativeGoRouteData {} diff --git a/packages/go_router_builder/test_inputs/no_mixin_relative.dart.expect b/packages/go_router_builder/test_inputs/no_mixin_relative.dart.expect new file mode 100644 index 00000000000..a9efb29f477 --- /dev/null +++ b/packages/go_router_builder/test_inputs/no_mixin_relative.dart.expect @@ -0,0 +1 @@ +Missing mixin clause `with _$RelativeRoute` diff --git a/packages/go_router_builder/test_inputs/relative_route_with_absolute_path.dart b/packages/go_router_builder/test_inputs/relative_route_with_absolute_path.dart new file mode 100644 index 00000000000..a5ea6bce2cc --- /dev/null +++ b/packages/go_router_builder/test_inputs/relative_route_with_absolute_path.dart @@ -0,0 +1,12 @@ +// 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. + +import 'package:go_router/go_router.dart'; + +mixin _$RelativeRoute {} + +@TypedRelativeGoRoute(path: '/relative-route') +class RelativeRoute extends RelativeGoRouteData with _$RelativeRoute { + const RelativeRoute(); +} diff --git a/packages/go_router_builder/test_inputs/relative_route_with_absolute_path.dart.expect b/packages/go_router_builder/test_inputs/relative_route_with_absolute_path.dart.expect new file mode 100644 index 00000000000..86fedafd5f8 --- /dev/null +++ b/packages/go_router_builder/test_inputs/relative_route_with_absolute_path.dart.expect @@ -0,0 +1 @@ +The path for a TypedRelativeGoRoute cannot start with "/". diff --git a/packages/go_router_builder/test_inputs/relative_route_with_direct_absolute_sub_route.dart b/packages/go_router_builder/test_inputs/relative_route_with_direct_absolute_sub_route.dart new file mode 100644 index 00000000000..e89034809c4 --- /dev/null +++ b/packages/go_router_builder/test_inputs/relative_route_with_direct_absolute_sub_route.dart @@ -0,0 +1,33 @@ +// 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. + +import 'package:go_router/go_router.dart'; + +mixin _$HomeRoute {} +mixin _$RelativeRoute {} +mixin _$NonRelativeRoute {} + +@TypedGoRoute( + path: '/', + routes: >[relativeRoute], +) +class HomeRoute extends GoRouteData with _$HomeRoute { + const HomeRoute(); +} + +const TypedRelativeGoRoute relativeRoute = + TypedRelativeGoRoute( + path: 'relative-route', + routes: >[ + TypedGoRoute(path: 'non-relative-route'), + ], + ); + +class RelativeRoute extends RelativeGoRouteData with _$RelativeRoute { + const RelativeRoute(); +} + +class NonRelativeRoute extends GoRouteData with _$NonRelativeRoute { + const NonRelativeRoute(); +} diff --git a/packages/go_router_builder/test_inputs/relative_route_with_direct_absolute_sub_route.dart.expect b/packages/go_router_builder/test_inputs/relative_route_with_direct_absolute_sub_route.dart.expect new file mode 100644 index 00000000000..5c43e6722f5 --- /dev/null +++ b/packages/go_router_builder/test_inputs/relative_route_with_direct_absolute_sub_route.dart.expect @@ -0,0 +1 @@ +TypedRelativeGoRoute cannot have a TypedGoRoute descendant. diff --git a/packages/go_router_builder/test_inputs/relative_route_with_indirect_absolute_sub_route.dart b/packages/go_router_builder/test_inputs/relative_route_with_indirect_absolute_sub_route.dart new file mode 100644 index 00000000000..decb0675931 --- /dev/null +++ b/packages/go_router_builder/test_inputs/relative_route_with_indirect_absolute_sub_route.dart @@ -0,0 +1,44 @@ +// 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. + +import 'package:go_router/go_router.dart'; + +mixin _$HomeRoute {} +mixin _$ShellRoute {} +mixin _$RelativeRoute {} +mixin _$AbsoluteRoute {} + +@TypedGoRoute( + path: '/', + routes: >[relativeRoute], +) +class HomeRoute extends GoRouteData with _$HomeRoute { + const HomeRoute(); +} + +const TypedRelativeGoRoute relativeRoute = + TypedRelativeGoRoute( + path: 'relative-route', + routes: >[shellRoute], + ); + +const TypedShellRoute shellRoute = TypedShellRoute( + routes: >[absoluteRoute], +); + +const TypedGoRoute absoluteRoute = TypedGoRoute( + path: 'absolute-route', +); + +class RelativeRoute extends RelativeGoRouteData with _$RelativeRoute { + const RelativeRoute(); +} + +class ShellRoute extends ShellRouteData with _$ShellRoute { + const ShellRoute(); +} + +class AbsoluteRoute extends GoRouteData with _$AbsoluteRoute { + const AbsoluteRoute(); +} diff --git a/packages/go_router_builder/test_inputs/relative_route_with_indirect_absolute_sub_route.dart.expect b/packages/go_router_builder/test_inputs/relative_route_with_indirect_absolute_sub_route.dart.expect new file mode 100644 index 00000000000..5c43e6722f5 --- /dev/null +++ b/packages/go_router_builder/test_inputs/relative_route_with_indirect_absolute_sub_route.dart.expect @@ -0,0 +1 @@ +TypedRelativeGoRoute cannot have a TypedGoRoute descendant.