diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index b1f15fba7d3..3fcea0b2dc6 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 + +* Adds Support for ShellRoute + ## 1.1.7 * Supports default values for `Set`, `List` and `Iterable` route parameters. diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index e7d3a96a617..e911d33be5c 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -327,3 +327,44 @@ class FancyRoute extends GoRouteData { ), } ``` + +## TypedShellRoute and navigator keys + +There may be situations were a child route of a shell needs to be displayed on a +different navigator. This kind of scenarios can be achieved by declaring a +**static** navigator key named: + +- `$navigatorKey` for ShellRoutes +- `$parentNavigatorKey` for GoRoutes + +Example: + +```dart +// For ShellRoutes: +final GlobalKey shellNavigatorKey = GlobalKey(); + +class MyShellRouteData extends ShellRouteData { + const MyShellRouteData(); + + static final GlobalKey $navigatorKey = shellNavigatorKey; + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + // ... + } +} + +// For GoRoutes: +class MyGoRouteData extends GoRouteData { + const MyGoRouteData(); + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Widget build(BuildContext context, GoRouterState state) { + // ... + } +} +``` + +An example is available [here](https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/shell_route_with_keys_example.dart). 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 f38dadc2021..0dc37d5e533 100644 --- a/packages/go_router_builder/example/lib/all_types.g.dart +++ b/packages/go_router_builder/example/lib/all_types.g.dart @@ -8,11 +8,11 @@ part of 'all_types.dart'; // GoRouterGenerator // ************************************************************************** -List get $appRoutes => [ +List get $appRoutes => [ $allTypesBaseRoute, ]; -GoRoute get $allTypesBaseRoute => GoRouteData.$route( +RouteBase get $allTypesBaseRoute => GoRouteData.$route( path: '/', factory: $AllTypesBaseRouteExtension._fromState, routes: [ diff --git a/packages/go_router_builder/example/lib/main.g.dart b/packages/go_router_builder/example/lib/main.g.dart index ff7a23627c9..ba15ccd83f7 100644 --- a/packages/go_router_builder/example/lib/main.g.dart +++ b/packages/go_router_builder/example/lib/main.g.dart @@ -8,12 +8,12 @@ part of 'main.dart'; // GoRouterGenerator // ************************************************************************** -List get $appRoutes => [ +List get $appRoutes => [ $homeRoute, $loginRoute, ]; -GoRoute get $homeRoute => GoRouteData.$route( +RouteBase get $homeRoute => GoRouteData.$route( path: '/', factory: $HomeRouteExtension._fromState, routes: [ @@ -118,7 +118,7 @@ extension on Map { entries.singleWhere((element) => element.value == value).key; } -GoRoute get $loginRoute => GoRouteData.$route( +RouteBase get $loginRoute => GoRouteData.$route( path: '/login', factory: $LoginRouteExtension._fromState, ); diff --git a/packages/go_router_builder/example/lib/shell_route_example.dart b/packages/go_router_builder/example/lib/shell_route_example.dart new file mode 100644 index 00000000000..2662ba83d37 --- /dev/null +++ b/packages/go_router_builder/example/lib/shell_route_example.dart @@ -0,0 +1,153 @@ +// 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 'shell_route_example.g.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + ); + + final GoRouter _router = GoRouter( + routes: $appRoutes, + initialLocation: '/foo', + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('foo')), + ); +} + +@TypedShellRoute( + routes: >[ + TypedGoRoute(path: '/foo'), + TypedGoRoute(path: '/bar'), + ], +) +class MyShellRouteData extends ShellRouteData { + const MyShellRouteData(); + + @override + Widget builder( + BuildContext context, + GoRouterState state, + Widget navigator, + ) { + return MyShellRouteScreen(child: navigator); + } +} + +class FooRouteData extends GoRouteData { + const FooRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const FooScreen(); + } +} + +class BarRouteData extends GoRouteData { + const BarRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const BarScreen(); + } +} + +class MyShellRouteScreen extends StatelessWidget { + const MyShellRouteScreen({required this.child, super.key}); + + final Widget child; + + int getCurrentIndex(BuildContext context) { + final String location = GoRouter.of(context).location; + if (location == '/bar') { + return 1; + } + return 0; + } + + @override + Widget build(BuildContext context) { + final int currentIndex = getCurrentIndex(context); + return Scaffold( + body: child, + bottomNavigationBar: BottomNavigationBar( + currentIndex: currentIndex, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'Foo', + ), + BottomNavigationBarItem( + icon: Icon(Icons.business), + label: 'Bar', + ), + ], + onTap: (int index) { + switch (index) { + case 0: + const FooRouteData().go(context); + break; + case 1: + const BarRouteData().go(context); + break; + } + }, + ), + ); + } +} + +class FooScreen extends StatelessWidget { + const FooScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Text('Foo'); + } +} + +class BarScreen extends StatelessWidget { + const BarScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Text('Bar'); + } +} + +@TypedGoRoute(path: '/login') +class LoginRoute extends GoRouteData { + const LoginRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => + const LoginScreen(); +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Text('Login'); + } +} diff --git a/packages/go_router_builder/example/lib/shell_route_example.g.dart b/packages/go_router_builder/example/lib/shell_route_example.g.dart new file mode 100644 index 00000000000..531502515a0 --- /dev/null +++ b/packages/go_router_builder/example/lib/shell_route_example.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'shell_route_example.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $loginRoute, + $myShellRouteData, + ]; + +RouteBase get $loginRoute => GoRouteData.$route( + path: '/login', + factory: $LoginRouteExtension._fromState, + ); + +extension $LoginRouteExtension on LoginRoute { + static LoginRoute _fromState(GoRouterState state) => const LoginRoute(); + + String get location => GoRouteData.$location( + '/login', + ); + + void go(BuildContext context) => context.go(location); + + void push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} + +RouteBase get $myShellRouteData => ShellRouteData.$route( + factory: $MyShellRouteDataExtension._fromState, + routes: [ + GoRouteData.$route( + path: '/foo', + factory: $FooRouteDataExtension._fromState, + ), + GoRouteData.$route( + path: '/bar', + factory: $BarRouteDataExtension._fromState, + ), + ], + ); + +extension $MyShellRouteDataExtension on MyShellRouteData { + static MyShellRouteData _fromState(GoRouterState state) => + const MyShellRouteData(); +} + +extension $FooRouteDataExtension on FooRouteData { + static FooRouteData _fromState(GoRouterState state) => const FooRouteData(); + + String get location => GoRouteData.$location( + '/foo', + ); + + void go(BuildContext context) => context.go(location); + + void push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} + +extension $BarRouteDataExtension on BarRouteData { + static BarRouteData _fromState(GoRouterState state) => const BarRouteData(); + + String get location => GoRouteData.$location( + '/bar', + ); + + 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/example/lib/shell_route_with_keys_example.dart b/packages/go_router_builder/example/lib/shell_route_with_keys_example.dart new file mode 100644 index 00000000000..bc6521e05fe --- /dev/null +++ b/packages/go_router_builder/example/lib/shell_route_with_keys_example.dart @@ -0,0 +1,169 @@ +// 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 'shell_route_with_keys_example.g.dart'; + +void main() => runApp(App()); + +final GlobalKey rootNavigatorKey = GlobalKey(); +final GlobalKey shellNavigatorKey = GlobalKey(); + +class App extends StatelessWidget { + App({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + ); + + final GoRouter _router = GoRouter( + routes: $appRoutes, + initialLocation: '/home', + navigatorKey: rootNavigatorKey, + ); +} + +@TypedShellRoute( + routes: >[ + TypedGoRoute(path: '/home'), + TypedGoRoute( + path: '/users', + routes: >[ + TypedGoRoute(path: ':id'), + ], + ), + ], +) +class MyShellRouteData extends ShellRouteData { + const MyShellRouteData(); + + static final GlobalKey $navigatorKey = shellNavigatorKey; + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + return MyShellRouteScreen(child: navigator); + } +} + +class MyShellRouteScreen extends StatelessWidget { + const MyShellRouteScreen({required this.child, super.key}); + + final Widget child; + + int getCurrentIndex(BuildContext context) { + final String location = GoRouter.of(context).location; + if (location.startsWith('/users')) { + return 1; + } + return 0; + } + + @override + Widget build(BuildContext context) { + final int selectedIndex = getCurrentIndex(context); + + return Scaffold( + body: Row( + children: [ + NavigationRail( + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.home), + label: Text('Home'), + ), + NavigationRailDestination( + icon: Icon(Icons.group), + label: Text('Users'), + ), + ], + selectedIndex: selectedIndex, + onDestinationSelected: (int index) { + switch (index) { + case 0: + const HomeRouteData().go(context); + break; + case 1: + const UsersRouteData().go(context); + break; + } + }, + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded(child: child), + ], + ), + ); + } +} + +class HomeRouteData extends GoRouteData { + const HomeRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const Center(child: Text('The home page')); + } +} + +class UsersRouteData extends GoRouteData { + const UsersRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return ListView( + children: [ + for (int userID = 1; userID <= 3; userID++) + ListTile( + title: Text('User $userID'), + onTap: () => UserRouteData(id: userID).go(context), + ), + ], + ); + } +} + +class DialogPage extends Page { + /// A page to display a dialog. + const DialogPage({required this.child, super.key}); + + /// The widget to be displayed which is usually a [Dialog] widget. + final Widget child; + + @override + Route createRoute(BuildContext context) { + return DialogRoute( + context: context, + settings: this, + builder: (BuildContext context) => child, + ); + } +} + +class UserRouteData extends GoRouteData { + const UserRouteData({required this.id}); + + // Without this static key, the dialog will not cover the navigation rail. + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + final int id; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return DialogPage( + key: state.pageKey, + child: Center( + child: SizedBox( + width: 300, + height: 300, + child: Card(child: Center(child: Text('User $id'))), + ), + ), + ); + } +} diff --git a/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart b/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart new file mode 100644 index 00000000000..523af4e9893 --- /dev/null +++ b/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart @@ -0,0 +1,88 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'shell_route_with_keys_example.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $myShellRouteData, + ]; + +RouteBase get $myShellRouteData => ShellRouteData.$route( + factory: $MyShellRouteDataExtension._fromState, + navigatorKey: MyShellRouteData.$navigatorKey, + routes: [ + GoRouteData.$route( + path: '/home', + factory: $HomeRouteDataExtension._fromState, + ), + GoRouteData.$route( + path: '/users', + factory: $UsersRouteDataExtension._fromState, + routes: [ + GoRouteData.$route( + path: ':id', + factory: $UserRouteDataExtension._fromState, + parentNavigatorKey: UserRouteData.$parentNavigatorKey, + ), + ], + ), + ], + ); + +extension $MyShellRouteDataExtension on MyShellRouteData { + static MyShellRouteData _fromState(GoRouterState state) => + const MyShellRouteData(); +} + +extension $HomeRouteDataExtension on HomeRouteData { + static HomeRouteData _fromState(GoRouterState state) => const HomeRouteData(); + + String get location => GoRouteData.$location( + '/home', + ); + + void go(BuildContext context) => context.go(location); + + void push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} + +extension $UsersRouteDataExtension on UsersRouteData { + static UsersRouteData _fromState(GoRouterState state) => + const UsersRouteData(); + + String get location => GoRouteData.$location( + '/users', + ); + + void go(BuildContext context) => context.go(location); + + void push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} + +extension $UserRouteDataExtension on UserRouteData { + static UserRouteData _fromState(GoRouterState state) => UserRouteData( + id: int.parse(state.params['id']!), + ); + + String get location => GoRouteData.$location( + '/users/${Uri.encodeComponent(id.toString())}', + ); + + 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/example/lib/simple_example.g.dart b/packages/go_router_builder/example/lib/simple_example.g.dart index 6f9eb6a5eef..4edd92ae6e1 100644 --- a/packages/go_router_builder/example/lib/simple_example.g.dart +++ b/packages/go_router_builder/example/lib/simple_example.g.dart @@ -8,11 +8,11 @@ part of 'simple_example.dart'; // GoRouterGenerator // ************************************************************************** -List get $appRoutes => [ +List get $appRoutes => [ $homeRoute, ]; -GoRoute get $homeRoute => GoRouteData.$route( +RouteBase get $homeRoute => GoRouteData.$route( path: '/', factory: $HomeRouteExtension._fromState, routes: [ diff --git a/packages/go_router_builder/example/pubspec.yaml b/packages/go_router_builder/example/pubspec.yaml index 4fd36fcab06..b8b7c9fa679 100644 --- a/packages/go_router_builder/example/pubspec.yaml +++ b/packages/go_router_builder/example/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: provider: 6.0.5 dev_dependencies: - build_runner: ^2.0.0 + build_runner: ^2.3.0 build_verify: ^3.1.0 flutter_test: sdk: flutter 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 ec25b3df468..99a3824cf4e 100644 --- a/packages/go_router_builder/example/test/all_types_test.dart +++ b/packages/go_router_builder/example/test/all_types_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// import 'package:flutter/material.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router_builder_example/all_types.dart'; diff --git a/packages/go_router_builder/example/test/shell_route_test.dart b/packages/go_router_builder/example/test/shell_route_test.dart new file mode 100644 index 00000000000..3919970209d --- /dev/null +++ b/packages/go_router_builder/example/test/shell_route_test.dart @@ -0,0 +1,17 @@ +// 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/shell_route_example.dart'; + +void main() { + testWidgets('Navigate from /foo to /bar', (WidgetTester tester) async { + await tester.pumpWidget(App()); + expect(find.byType(FooScreen), findsOneWidget); + + await tester.tap(find.text('Bar')); + await tester.pumpAndSettle(); + expect(find.byType(BarScreen), findsOneWidget); + }); +} diff --git a/packages/go_router_builder/example/test/shell_route_with_keys_test.dart b/packages/go_router_builder/example/test/shell_route_with_keys_test.dart new file mode 100644 index 00000000000..2c7bd59aa89 --- /dev/null +++ b/packages/go_router_builder/example/test/shell_route_with_keys_test.dart @@ -0,0 +1,18 @@ +// 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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_builder_example/shell_route_with_keys_example.dart'; + +void main() { + testWidgets('Navigate from /home to /users', (WidgetTester tester) async { + await tester.pumpWidget(App()); + expect(find.text('The home page'), findsOneWidget); + + await tester.tap(find.byIcon(Icons.group)); + await tester.pumpAndSettle(); + expect(find.byType(ListTile), findsNWidgets(3)); + }); +} 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 42ef4ca343c..5a1767022db 100644 --- a/packages/go_router_builder/lib/src/go_router_generator.dart +++ b/packages/go_router_builder/lib/src/go_router_generator.dart @@ -11,34 +11,33 @@ import 'package:source_gen/source_gen.dart'; import 'route_config.dart'; -/// A [Generator] for classes annotated with `TypedGoRoute`. +const String _routeDataUrl = 'package:go_router/src/route_data.dart'; + +const Map _annotations = { + 'TypedGoRoute': 'GoRouteData', + 'TypedShellRoute': 'ShellRouteData', +}; + +/// A [Generator] for classes annotated with a typed go route annotation. class GoRouterGenerator extends GeneratorForAnnotation { /// Creates a new instance of [GoRouterGenerator]. const GoRouterGenerator(); @override - TypeChecker get typeChecker => const TypeChecker.fromUrl( - 'package:go_router/src/route_data.dart#TypedGoRoute', + TypeChecker get typeChecker => TypeChecker.any( + _annotations.keys.map((String annotation) => + TypeChecker.fromUrl('$_routeDataUrl#$annotation')), ); @override FutureOr generate(LibraryReader library, BuildStep buildStep) async { final Set values = {}; - final Set getters = {}; - for (final AnnotatedElement annotatedElement - in library.annotatedWith(typeChecker)) { - final InfoIterable generatedValue = generateForAnnotatedElement( - annotatedElement.element, - annotatedElement.annotation, - buildStep, - ); - getters.add(generatedValue.routeGetterName); - for (final String value in generatedValue) { - assert(value.length == value.trim().length); - values.add(value); - } + for (final String annotation in _annotations.keys) { + final TypeChecker typeChecker = + TypeChecker.fromUrl('$_routeDataUrl#$annotation'); + _generateForAnnotation(library, typeChecker, buildStep, values, getters); } if (values.isEmpty) { @@ -47,7 +46,7 @@ class GoRouterGenerator extends GeneratorForAnnotation { return [ ''' -List get \$appRoutes => [ +List get \$appRoutes => [ ${getters.map((String e) => "$e,").join('\n')} ]; ''', @@ -55,24 +54,53 @@ ${getters.map((String e) => "$e,").join('\n')} ].join('\n\n'); } + void _generateForAnnotation( + LibraryReader library, + TypeChecker typeChecker, + BuildStep buildStep, + Set values, + Set getters, + ) { + for (final AnnotatedElement annotatedElement + in library.annotatedWith(typeChecker)) { + final InfoIterable generatedValue = generateForAnnotatedElement( + annotatedElement.element, + annotatedElement.annotation, + buildStep, + ); + getters.add(generatedValue.routeGetterName); + for (final String value in generatedValue) { + assert(value.length == value.trim().length); + values.add(value); + } + } + } + @override InfoIterable generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep, ) { + final String typedAnnotation = + annotation.objectValue.type!.getDisplayString(withNullability: false); + final String type = + typedAnnotation.substring(0, typedAnnotation.indexOf('<')); + final String routeData = _annotations[type]!; if (element is! ClassElement) { throw InvalidGenerationSourceError( - 'The @TypedGoRoute annotation can only be applied to classes.', + 'The @$type annotation can only be applied to classes.', element: element, ); } - if (!element.allSupertypes.any((InterfaceType element) => - _goRouteDataChecker.isExactlyType(element))) { + final TypeChecker dataChecker = + TypeChecker.fromUrl('$_routeDataUrl#$routeData'); + if (!element.allSupertypes + .any((InterfaceType element) => dataChecker.isExactlyType(element))) { throw InvalidGenerationSourceError( - 'The @TypedGoRoute annotation can only be applied to classes that ' - 'extend or implement `GoRouteData`.', + 'The @$type annotation can only be applied to classes that ' + 'extend or implement `$routeData`.', element: element, ); } @@ -80,7 +108,3 @@ ${getters.map((String e) => "$e,").join('\n')} return RouteConfig.fromAnnotation(annotation, element).generateMembers(); } } - -const TypeChecker _goRouteDataChecker = TypeChecker.fromUrl( - 'package:go_router/src/route_data.dart#GoRouteData', -); diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index cfc4863fedf..9d7c1d97b16 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -39,6 +39,8 @@ class RouteConfig { this._path, this._routeDataClass, this._parent, + this._key, + this._isShellRoute, ); /// Creates a new [RouteConfig] represented the annotation data in [reader]. @@ -66,19 +68,26 @@ class RouteConfig { RouteConfig? parent, ) { assert(!reader.isNull, 'reader should not be null'); - final ConstantReader pathValue = reader.read('path'); - if (pathValue.isNull) { - throw InvalidGenerationSourceError( - 'Missing `path` value on annotation.', - element: element, - ); - } + final InterfaceType type = reader.objectValue.type! as InterfaceType; + // TODO(stuartmorgan): Remove this ignore once 'analyze' can be set to + // 5.2+ (when Flutter 3.4+ is on stable). + // ignore: deprecated_member_use + final bool isShellRoute = type.element.name == 'TypedShellRoute'; - final String path = pathValue.stringValue; + String? path; - final InterfaceType type = reader.objectValue.type! as InterfaceType; - final DartType typeParamType = type.typeArguments.single; + if (!isShellRoute) { + final ConstantReader pathValue = reader.read('path'); + if (pathValue.isNull) { + throw InvalidGenerationSourceError( + 'Missing `path` value on annotation.', + element: element, + ); + } + path = pathValue.stringValue; + } + final DartType typeParamType = type.typeArguments.single; if (typeParamType is! InterfaceType) { throw InvalidGenerationSourceError( 'The type parameter on one of the @TypedGoRoute declarations could not ' @@ -93,7 +102,16 @@ class RouteConfig { // ignore: deprecated_member_use final InterfaceElement classElement = typeParamType.element; - final RouteConfig value = RouteConfig._(path, classElement, parent); + final RouteConfig value = RouteConfig._( + path ?? '', + classElement, + parent, + _generateNavigatorKeyGetterCode( + classElement, + keyName: isShellRoute ? r'$navigatorKey' : r'$parentNavigatorKey', + ), + isShellRoute, + ); value._children.addAll(reader.read('routes').listValue.map((DartObject e) => RouteConfig._fromAnnotation(ConstantReader(e), element, value))); @@ -105,6 +123,42 @@ class RouteConfig { final String _path; final InterfaceElement _routeDataClass; final RouteConfig? _parent; + final String? _key; + final bool _isShellRoute; + + static String? _generateNavigatorKeyGetterCode( + InterfaceElement classElement, { + required String keyName, + }) { + bool whereStatic(FieldElement element) => element.isStatic; + bool whereKeyName(FieldElement element) => element.name == keyName; + final String? fieldDisplayName = classElement.fields + .where(whereStatic) + .where(whereKeyName) + .where((FieldElement element) { + final DartType type = element.type; + if (type is! ParameterizedType) { + return false; + } + final List typeArguments = type.typeArguments; + if (typeArguments.length != 1) { + return false; + } + final DartType typeArgument = typeArguments.single; + if (typeArgument.getDisplayString(withNullability: false) == + 'NavigatorState') { + return true; + } + return false; + }) + .map((FieldElement e) => e.displayName) + .firstOrNull; + + if (fieldDisplayName == null) { + return null; + } + return '${classElement.name}.$fieldDisplayName'; + } /// Generates all of the members that correspond to `this`. InfoIterable generateMembers() => InfoIterable._( @@ -136,7 +190,15 @@ class RouteConfig { } /// Returns `extension` code. - String _extensionDefinition() => ''' + String _extensionDefinition() { + if (_isShellRoute) { + return ''' +extension $_extensionName on $_className { + static $_className _fromState(GoRouterState state) $_newFromState +} +'''; + } + return ''' extension $_extensionName on $_className { static $_className _fromState(GoRouterState state) $_newFromState @@ -152,6 +214,7 @@ extension $_extensionName on $_className { context.pushReplacement(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); } '''; + } /// Returns this [RouteConfig] and all child [RouteConfig] instances. Iterable _flatten() sync* { @@ -166,7 +229,7 @@ extension $_extensionName on $_className { /// Returns the `GoRoute` code for the annotated class. String _rootDefinition() => ''' -GoRoute get $_routeGetterName => ${_routeDefinition()}; +RouteBase get $_routeGetterName => ${_routeDefinition()}; '''; /// Returns code representing the constant maps that contain the `enum` to @@ -271,11 +334,25 @@ GoRoute get $_routeGetterName => ${_routeDefinition()}; : ''' routes: [${_children.map((RouteConfig e) => '${e._routeDefinition()},').join()}], '''; - + final String navigatorKeyParameterName = + _isShellRoute ? 'navigatorKey' : 'parentNavigatorKey'; + final String navigatorKey = _key == null || _key!.isEmpty + ? '' + : '$navigatorKeyParameterName: $_key,'; + if (_isShellRoute) { + return ''' + ShellRouteData.\$route( + factory: $_extensionName._fromState, + $navigatorKey + $routesBit + ) +'''; + } return ''' GoRouteData.\$route( path: ${escapeDartString(_path)}, factory: $_extensionName._fromState, + $navigatorKey $routesBit ) '''; diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 5bc629aa393..99d3ba34f5d 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.7 +version: 1.2.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 @@ -10,7 +10,7 @@ environment: sdk: ">=2.18.0 <4.0.0" dependencies: - analyzer: '>=4.4.0 <6.0.0' + analyzer: ">=4.4.0 <6.0.0" async: ^2.8.0 build: ^2.0.0 build_config: ^1.0.0 @@ -23,6 +23,6 @@ dependencies: dev_dependencies: build_runner: ^2.0.0 - go_router: ^5.0.0 + go_router: ^6.0.10 source_gen_test: ^1.0.0 test: ^1.20.0 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 9875d2e921e..2b843c1e8d7 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 @@ -72,7 +72,7 @@ class MissingPathParam extends GoRouteData { } @ShouldGenerate(r''' -GoRoute get $enumParam => GoRouteData.$route( +RouteBase get $enumParam => GoRouteData.$route( path: '/:y', factory: $EnumParamExtension._fromState, ); @@ -121,7 +121,7 @@ enum EnumTest { } @ShouldGenerate(r''' -GoRoute get $defaultValueRoute => GoRouteData.$route( +RouteBase get $defaultValueRoute => GoRouteData.$route( path: '/default-value-route', factory: $DefaultValueRouteExtension._fromState, ); @@ -162,7 +162,7 @@ class DefaultValueRoute extends GoRouteData { } @ShouldGenerate(r''' -GoRoute get $extraValueRoute => GoRouteData.$route( +RouteBase get $extraValueRoute => GoRouteData.$route( path: '/default-value-route', factory: $ExtraValueRouteExtension._fromState, ); @@ -215,7 +215,7 @@ class NullableDefaultValueRoute extends GoRouteData { } @ShouldGenerate(r''' -GoRoute get $iterableWithEnumRoute => GoRouteData.$route( +RouteBase get $iterableWithEnumRoute => GoRouteData.$route( path: '/iterable-with-enum', factory: $IterableWithEnumRouteExtension._fromState, ); @@ -269,7 +269,7 @@ enum EnumOnlyUsedInIterable { } @ShouldGenerate(r''' -GoRoute get $iterableDefaultValueRoute => GoRouteData.$route( +RouteBase get $iterableDefaultValueRoute => GoRouteData.$route( path: '/iterable-default-value-route', factory: $IterableDefaultValueRouteExtension._fromState, );