diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 7134ea8aa4c9..fc8d5b8d548e 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.6.5 + +- Adds `TypedRelativeGoRoute` + ## 14.6.4 - Rephrases readme. diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index afe6159cecb9..c6c2e13845b4 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -390,6 +390,28 @@ class TypedGoRoute extends TypedRoute { final List> routes; } +/// A superclass for each typed go route descendant +@Target({TargetKind.library, TargetKind.classType}) +class TypedRelativeGoRoute extends TypedRoute { + /// Default const constructor + const TypedRelativeGoRoute({ + required this.path, + this.routes = const >[], + }); + + /// The relative path that corresponds to this route. + /// + /// See [GoRoute.path]. + /// + /// + final String path; + + /// Child route definitions. + /// + /// See [RouteBase.routes]. + final List> routes; +} + /// A superclass for each typed shell route descendant @Target({TargetKind.library, TargetKind.classType}) class TypedShellRoute extends TypedRoute { diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index ef2d9422b465..7600be29884b 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 14.6.4 +version: 14.6.5 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 687d9210f05f..2e6cb9d8e0d1 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.7.4 + +- Adds `TypedRelativeGoRoute` annotation which supports relative routes. ## 2.7.3 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 000000000000..26f49772d507 --- /dev/null +++ b/packages/go_router_builder/example/lib/go_relative.dart @@ -0,0 +1,198 @@ +// 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 { + @override + Widget build(BuildContext context, GoRouterState state) { + return const HomeScreen(); + } +} + +class DashboardRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const DashboardScreen(); + } +} + +class DetailsRoute extends GoRouteData { + const DetailsRoute({required this.detailId}); + final String detailId; + + @override + Widget build(BuildContext context, GoRouterState state) { + return DetailsScreen(id: detailId); + } +} + +class SettingsRoute extends GoRouteData { + 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 000000000000..1a3907739fa9 --- /dev/null +++ b/packages/go_router_builder/example/lib/go_relative.g.dart @@ -0,0 +1,122 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'go_relative.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $homeRoute, + ]; + +RouteBase get $homeRoute => GoRouteData.$route( + path: '/', + factory: $HomeRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: '/dashboard', + factory: $DashboardRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'details/:detailId', + factory: $DetailsRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'settings/:settingId', + factory: $SettingsRouteExtension._fromState, + ), + ], + ), + ], + ), + GoRouteData.$route( + path: 'details/:detailId', + factory: $DetailsRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'settings/:settingId', + factory: $SettingsRouteExtension._fromState, + ), + ], + ), + ], + ); + +extension $HomeRouteExtension on HomeRoute { + static HomeRoute _fromState(GoRouterState state) => HomeRoute(); + + String get location => GoRouteData.$location( + '/', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $DashboardRouteExtension on DashboardRoute { + static DashboardRoute _fromState(GoRouterState state) => DashboardRoute(); + + String get location => GoRouteData.$location( + '/dashboard', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $DetailsRouteExtension on DetailsRoute { + static DetailsRoute _fromState(GoRouterState state) => DetailsRoute( + detailId: state.pathParameters['detailId']!, + ); + + String get location => GoRouteData.$location( + 'details/${Uri.encodeComponent(detailId)}', + ); + String get relativeLocation => './$location'; + + void goRelative(BuildContext context) => context.go(relativeLocation); + + Future pushRelative(BuildContext context) => + context.push(relativeLocation); + + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + void replaceRelative(BuildContext context) => + context.replace(relativeLocation); +} + +extension $SettingsRouteExtension on SettingsRoute { + static SettingsRoute _fromState(GoRouterState state) => SettingsRoute( + settingId: state.pathParameters['settingId']!, + ); + + String get location => GoRouteData.$location( + 'settings/${Uri.encodeComponent(settingId)}', + ); + String get relativeLocation => './$location'; + + void goRelative(BuildContext context) => context.go(relativeLocation); + + Future pushRelative(BuildContext context) => + context.push(relativeLocation); + + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + 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 46d64d191886..48d9d5c16ee2 100644 --- a/packages/go_router_builder/example/pubspec.yaml +++ b/packages/go_router_builder/example/pubspec.yaml @@ -21,5 +21,9 @@ dev_dependencies: path: .. test: ^1.17.0 +dependency_overrides: + go_router: + path: ../../go_router + flutter: uses-material-design: true 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 000000000000..3efd323e8c23 --- /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 e094d1f98edd..1032b78873d8 100644 --- a/packages/go_router_builder/lib/src/go_router_generator.dart +++ b/packages/go_router_builder/lib/src/go_router_generator.dart @@ -15,6 +15,7 @@ const String _routeDataUrl = 'package:go_router/src/route_data.dart'; const Map _annotations = { 'TypedGoRoute': 'GoRouteData', + 'TypedRelativeRoute': 'GoRouteData', '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 0cfa7a3928cb..f341f0940ec2 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -209,8 +209,10 @@ class GoRouteConfig extends RouteBaseConfig { RouteBaseConfig? config = this; while (config != null) { - if (config is GoRouteConfig) { - pathSegments.add(config.path); + if (config + case GoRouteConfig(:final String path) || + GoRelativeRouteConfig(:final String path)) { + pathSegments.add(path); } config = config.parent; } @@ -424,6 +426,229 @@ extension $_extensionName on $_className { String get dataConvertionFunctionName => r'$route'; } +/// The configuration to generate class declarations for a GoRouteData. +class GoRelativeRouteConfig extends RouteBaseConfig { + GoRelativeRouteConfig._({ + required this.path, + 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 parent navigator key. + final String? parentNavigatorKey; + + late final Set _pathParams = pathParametersFromPattern(path); + + // construct path bits using parent bits + // if there are any queryParam objects, add in the `queryParam` bits + String get _locationArgs { + final Map pathParameters = Map.fromEntries( + _pathParams.map((String pathParameter) { + // Enum types are encoded using a map, so we need a nullability check + // here to ensure it matches Uri.encodeComponent nullability + final DartType? type = _field(pathParameter)?.returnType; + final String value = + '\${Uri.encodeComponent(${_encodeFor(pathParameter)}${type?.isEnum ?? false ? '!' : ''})}'; + return MapEntry(pathParameter, value); + }), + ); + final String location = patternToPath(path, pathParameters); + return "'$location'"; + } + + ParameterElement? get _extraParam => _ctor.parameters + .singleWhereOrNull((ParameterElement element) => element.isExtraField); + + String get _fromStateConstructor { + final StringBuffer buffer = StringBuffer('=>'); + if (_ctor.isConst && + _ctorParams.isEmpty && + _ctorQueryParams.isEmpty && + _extraParam == null) { + buffer.writeln('const '); + } + + buffer.writeln('$_className('); + for (final ParameterElement param in [ + ..._ctorParams, + ..._ctorQueryParams, + if (_extraParam != null) _extraParam!, + ]) { + buffer.write(_decodeFor(param)); + } + buffer.writeln(');'); + + return buffer.toString(); + } + + String _decodeFor(ParameterElement element) { + if (element.isRequired) { + if (element.type.nullabilitySuffix == NullabilitySuffix.question && + _pathParams.contains(element.name)) { + throw InvalidGenerationSourceError( + 'Required parameters in the path cannot be nullable.', + element: element, + ); + } + } + final String fromStateExpression = decodeParameter(element, _pathParams); + + if (element.isPositional) { + return '$fromStateExpression,'; + } + + if (element.isNamed) { + return '${element.name}: $fromStateExpression,'; + } + + throw InvalidGenerationSourceError( + '$likelyIssueMessage (param not named or positional)', + element: element, + ); + } + + String _encodeFor(String fieldName) { + final PropertyAccessorElement? field = _field(fieldName); + if (field == null) { + throw InvalidGenerationSourceError( + 'Could not find a field for the path parameter "$fieldName".', + element: routeDataClass, + ); + } + + return encodeField(field); + } + + String get _locationQueryParams { + if (_ctorQueryParams.isEmpty) { + return ''; + } + + final StringBuffer buffer = StringBuffer('queryParams: {\n'); + + for (final ParameterElement param in _ctorQueryParams) { + final String parameterName = param.name; + + final List conditions = []; + if (param.hasDefaultValue) { + if (param.type.isNullableType) { + throw NullableDefaultValueError(param); + } + conditions.add('$parameterName != ${param.defaultValueCode!}'); + } else if (param.type.isNullableType) { + conditions.add('$parameterName != null'); + } + String line = ''; + if (conditions.isNotEmpty) { + line = 'if (${conditions.join(' && ')}) '; + } + line += '${escapeDartString(parameterName.kebab)}: ' + '${_encodeFor(parameterName)},'; + + buffer.writeln(line); + } + + buffer.writeln('},'); + + return buffer.toString(); + } + + late final List _ctorParams = + _ctor.parameters.where((ParameterElement element) { + if (_pathParams.contains(element.name)) { + return true; + } + return false; + }).toList(); + + late final List _ctorQueryParams = _ctor.parameters + .where((ParameterElement element) => + !_pathParams.contains(element.name) && !element.isExtraField) + .toList(); + + ConstructorElement get _ctor { + final ConstructorElement? ctor = routeDataClass.unnamedConstructor; + + if (ctor == null) { + throw InvalidGenerationSourceError( + 'Missing default constructor', + element: routeDataClass, + ); + } + return ctor; + } + + @override + Iterable classDeclarations() => [ + _extensionDefinition, + ..._enumDeclarations(), + ]; + + String get _extensionDefinition => ''' +extension $_extensionName on $_className { + static $_className _fromState(GoRouterState state) $_fromStateConstructor + + String get location => GoRouteData.\$location($_locationArgs,$_locationQueryParams); + String get relativeLocation => './\$location'; + + void goRelative(BuildContext context) => + context.go(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + Future pushRelative(BuildContext context) => + context.push(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + void replaceRelative(BuildContext context) => + context.replace(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); +} +'''; + + /// Returns code representing the constant maps that contain the `enum` to + /// [String] mapping for each referenced enum. + Iterable _enumDeclarations() { + final Set enumParamTypes = {}; + + for (final ParameterElement 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); + } + } + return enumParamTypes.map(_enumMapConst); + } + + @override + String get factorConstructorParameters => + 'factory: $_extensionName._fromState,'; + + @override + String get routeConstructorParameters => ''' + path: ${escapeDartString(path)}, + ${parentNavigatorKey == null ? '' : 'parentNavigatorKey: $parentNavigatorKey,'} +'''; + + @override + String get routeDataClassName => 'GoRouteData'; + + @override + String get dataConvertionFunctionName => r'$route'; +} + /// Represents a `TypedGoRoute` annotation to the builder. abstract class RouteBaseConfig { RouteBaseConfig._({ @@ -550,6 +775,23 @@ 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, + ); + } + value = GoRelativeRouteConfig._( + path: pathValue.stringValue, + routeDataClass: classElement, + parent: parent, + parentNavigatorKey: _generateParameterGetterCode( + classElement, + parameterName: r'$parentNavigatorKey', + ), + ); default: throw UnsupportedError('Unrecognized type $typeName'); } diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 692b81f07967..19889010b8b2 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: 2.7.3 +version: 2.8.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 @@ -26,12 +26,16 @@ dev_dependencies: dart_style: '>=2.3.7 <4.0.0' flutter: sdk: flutter - go_router: ^14.0.0 + go_router: ^14.6.3 leak_tracker_flutter_testing: ">=3.0.0" package_config: ^2.1.1 pub_semver: ^2.1.5 test: ^1.20.0 +dependency_overrides: + go_router: + path: ../go_router + topics: - codegen - deep-linking 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 000000000000..f45c0f2569cb --- /dev/null +++ b/packages/go_router_builder/test_inputs/go_relative.dart @@ -0,0 +1,37 @@ +// 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'; + +const TypedRelativeGoRoute relativeRoute = + TypedRelativeGoRoute( + path: 'relative-route', + routes: >[ + TypedRelativeGoRoute(path: 'inner-relative-route') + ], +); + +@TypedGoRoute( + path: 'route-1', + routes: >[relativeRoute], +) +class Route1 extends GoRouteData { + const Route1(); +} + +@TypedGoRoute( + path: 'route-2', + routes: >[relativeRoute], +) +class Route2 extends GoRouteData { + const Route2(); +} + +class RelativeRoute extends GoRouteData { + const RelativeRoute(); +} + +class InnerRelativeRoute extends GoRouteData { + 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 000000000000..5e1275f5e24b --- /dev/null +++ b/packages/go_router_builder/test_inputs/go_relative.dart.expect @@ -0,0 +1,104 @@ +RouteBase get $route1 => GoRouteData.$route( + path: 'route-1', + factory: $Route1Extension._fromState, + routes: [ + GoRouteData.$route( + path: 'relative-route', + factory: $RelativeRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'inner-relative-route', + factory: $InnerRelativeRouteExtension._fromState, + ), + ], + ), + ], + ); + +extension $Route1Extension on Route1 { + static Route1 _fromState(GoRouterState state) => const Route1(); + + String get location => GoRouteData.$location( + 'route-1', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $RelativeRouteExtension on RelativeRoute { + static RelativeRoute _fromState(GoRouterState state) => const RelativeRoute(); + + String get location => GoRouteData.$location( + 'relative-route', + ); + String get relativeLocation => './$location'; + + void goRelative(BuildContext context) => context.go(relativeLocation); + + Future pushRelative(BuildContext context) => context.push(relativeLocation); + + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + void replaceRelative(BuildContext context) => context.replace(relativeLocation); +} + +extension $InnerRelativeRouteExtension on InnerRelativeRoute { + static InnerRelativeRoute _fromState(GoRouterState state) => + const InnerRelativeRoute(); + + String get location => GoRouteData.$location( + 'inner-relative-route', + ); + String get relativeLocation => './$location'; + + void goRelative(BuildContext context) => context.go(relativeLocation); + + Future pushRelative(BuildContext context) => context.push(relativeLocation); + + void pushReplacementRelative(BuildContext context) => + context.pushReplacement(relativeLocation); + + void replaceRelative(BuildContext context) => context.replace(relativeLocation); +} + +RouteBase get $route2 => GoRouteData.$route( + path: 'route-2', + factory: $Route2Extension._fromState, + routes: [ + GoRouteData.$route( + path: 'relative-route', + factory: $RelativeRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'inner-relative-route', + factory: $InnerRelativeRouteExtension._fromState, + ), + ], + ), + ], + ); + +extension $Route2Extension on Route2 { + static Route2 _fromState(GoRouterState state) => const Route2(); + + String get location => GoRouteData.$location( + 'route-2', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +}