From 1884e2c34e6009ef6d64a8273a651692b003709d Mon Sep 17 00:00:00 2001 From: thangmoxielabs Date: Wed, 6 Dec 2023 01:57:52 +0700 Subject: [PATCH 1/5] add `transitionDelegate` to `GoRouter` and penetrate it through layers to pass to `Navigator` --- packages/go_router/lib/src/builder.dart | 51 ++++++++++++++++-------- packages/go_router/lib/src/delegate.dart | 2 + packages/go_router/lib/src/router.dart | 4 ++ 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index e3e116fe758..98e018cbf97 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -46,6 +46,7 @@ class RouteBuilder { required this.observers, required this.onPopPageWithRouteMatch, this.requestFocus = true, + this.transitionDelegate, }); /// Builder function for a go router with Navigator. @@ -91,6 +92,9 @@ class RouteBuilder { final Map, HeroController> _goHeroCache = , HeroController>{}; + /// Pass this down to [Navigator.transitionDelegate] + final TransitionDelegate? transitionDelegate; + /// Builds the top-level Navigator for the given [RouteMatchList]. Widget build( BuildContext context, @@ -111,7 +115,8 @@ class RouteBuilder { final Map, GoRouterState> newRegistry = , GoRouterState>{}; final Widget result = tryBuild(context, matchList, routerNeglect, - configuration.navigatorKey, newRegistry); + configuration.navigatorKey, newRegistry, + transitionDelegate: transitionDelegate); _registry.updateRegistry(newRegistry); return GoRouterStateRegistryScope(registry: _registry, child: result); }, @@ -129,8 +134,9 @@ class RouteBuilder { RouteMatchList matchList, bool routerNeglect, GlobalKey navigatorKey, - Map, GoRouterState> registry, - ) { + Map, GoRouterState> registry, { + TransitionDelegate? transitionDelegate, + }) { // TODO(chunhtai): move the state from local scope to a central place. // https://github.com/flutter/flutter/issues/126365 final _PagePopContext pagePopContext = @@ -140,11 +146,13 @@ class RouteBuilder { _buildNavigator( pagePopContext.onPopPage, _buildPages(context, matchList, pagePopContext, routerNeglect, - navigatorKey, registry), + navigatorKey, registry, + transitionDelegate: transitionDelegate), navigatorKey, observers: observers, restorationScopeId: restorationScopeId, requestFocus: requestFocus, + transitionDelegate: transitionDelegate, ), ); } @@ -152,12 +160,14 @@ class RouteBuilder { /// Returns the top-level pages instead of the root navigator. Used for /// testing. List> _buildPages( - BuildContext context, - RouteMatchList matchList, - _PagePopContext pagePopContext, - bool routerNeglect, - GlobalKey navigatorKey, - Map, GoRouterState> registry) { + BuildContext context, + RouteMatchList matchList, + _PagePopContext pagePopContext, + bool routerNeglect, + GlobalKey navigatorKey, + Map, GoRouterState> registry, { + TransitionDelegate? transitionDelegate, + }) { final Map, List>> keyToPage; if (matchList.isError) { keyToPage = , List>>{ @@ -168,7 +178,8 @@ class RouteBuilder { } else { keyToPage = , List>>{}; _buildRecursive(context, matchList, 0, pagePopContext, routerNeglect, - keyToPage, navigatorKey, registry); + keyToPage, navigatorKey, registry, + transitionDelegate: transitionDelegate); // Every Page should have a corresponding RouteMatch. assert(keyToPage.values.flattened.every((Page page) => @@ -206,8 +217,9 @@ class RouteBuilder { bool routerNeglect, Map, List>> keyToPages, GlobalKey navigatorKey, - Map, GoRouterState> registry, - ) { + Map, GoRouterState> registry, { + TransitionDelegate? transitionDelegate, + }) { if (startIndex >= matchList.matches.length) { return; } @@ -220,7 +232,8 @@ class RouteBuilder { page = _buildErrorPage(context, state); keyToPages.putIfAbsent(navigatorKey, () => >[]).add(page); _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, navigatorKey, registry); + routerNeglect, keyToPages, navigatorKey, registry, + transitionDelegate: transitionDelegate); } else { // If this RouteBase is for a different Navigator, add it to the // list of out of scope pages @@ -237,7 +250,8 @@ class RouteBuilder { } _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, navigatorKey, registry); + routerNeglect, keyToPages, navigatorKey, registry, + transitionDelegate: transitionDelegate); } else if (route is ShellRouteBase) { assert(startIndex + 1 < matchList.matches.length, 'Shell routes must always have child routes'); @@ -259,7 +273,8 @@ class RouteBuilder { // Build the remaining pages _buildRecursive(context, matchList, startIndex + 1, pagePopContext, - routerNeglect, keyToPages, shellNavigatorKey, registry); + routerNeglect, keyToPages, shellNavigatorKey, registry, + transitionDelegate: transitionDelegate); final HeroController heroController = _goHeroCache.putIfAbsent( shellNavigatorKey, () => _getHeroController(context)); @@ -278,6 +293,7 @@ class RouteBuilder { restorationScopeId: restorationScopeId, heroController: heroController, requestFocus: requestFocus, + transitionDelegate: transitionDelegate, ); } @@ -312,6 +328,7 @@ class RouteBuilder { String? restorationScopeId, HeroController? heroController, bool requestFocus = true, + TransitionDelegate? transitionDelegate, }) { final Widget navigator = Navigator( key: navigatorKey, @@ -320,6 +337,8 @@ class RouteBuilder { observers: observers, onPopPage: onPopPage, requestFocus: requestFocus, + transitionDelegate: + transitionDelegate ?? const DefaultTransitionDelegate(), ); if (heroController != null) { return HeroControllerScope( diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 756eba28d79..40fb04ab0ab 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -28,6 +28,7 @@ class GoRouterDelegate extends RouterDelegate required this.routerNeglect, String? restorationScopeId, bool requestFocus = true, + TransitionDelegate? transitionDelegate, }) : _configuration = configuration { builder = RouteBuilder( configuration: configuration, @@ -38,6 +39,7 @@ class GoRouterDelegate extends RouterDelegate observers: observers, onPopPageWithRouteMatch: _handlePopPageWithRouteMatch, requestFocus: requestFocus, + transitionDelegate: transitionDelegate, ); } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index dc6d88057eb..9d4564eb69c 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -138,6 +138,7 @@ class GoRouter implements RouterConfig { GlobalKey? navigatorKey, String? restorationScopeId, bool requestFocus = true, + TransitionDelegate? transitionDelegate, }) { return GoRouter.routingConfig( routingConfig: _ConstantRoutingConfig( @@ -160,6 +161,7 @@ class GoRouter implements RouterConfig { navigatorKey: navigatorKey, restorationScopeId: restorationScopeId, requestFocus: requestFocus, + transitionDelegate: transitionDelegate, ); } @@ -182,6 +184,7 @@ class GoRouter implements RouterConfig { GlobalKey? navigatorKey, String? restorationScopeId, bool requestFocus = true, + TransitionDelegate? transitionDelegate, }) : _routingConfig = routingConfig, backButtonDispatcher = RootBackButtonDispatcher(), assert( @@ -242,6 +245,7 @@ class GoRouter implements RouterConfig { ], restorationScopeId: restorationScopeId, requestFocus: requestFocus, + transitionDelegate: transitionDelegate, // wrap the returned Navigator to enable GoRouter.of(context).go() et al, // allowing the caller to wrap the navigator themselves builderWithNav: (BuildContext context, Widget child) => From 50416dd87dbc6058aaea3bef7f0646b6842369e4 Mon Sep 17 00:00:00 2001 From: Thang Date: Tue, 28 May 2024 23:28:50 +0700 Subject: [PATCH 2/5] add [GoRouter.goRelative] and tests. Add TypedRelativeGoRoute and its builder without tests --- .../lib/src/information_provider.dart | 27 ++ .../go_router/lib/src/misc/extensions.dart | 7 + packages/go_router/lib/src/route_data.dart | 11 + packages/go_router/lib/src/router.dart | 9 + packages/go_router/test/go_router_test.dart | 277 ++++++++++++++++++ .../lib/src/go_router_generator.dart | 1 + .../lib/src/route_config.dart | 246 ++++++++++++++++ 7 files changed, 578 insertions(+) diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index dc979193b32..999cd530a25 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -172,6 +172,33 @@ class GoRouteInformationProvider extends RouteInformationProvider ); } + /// Relatively go to [relativeLocation]. + void goRelative(String relativeLocation, {Object? extra}) { + assert( + !relativeLocation.startsWith('/'), + "Relative locations must not start with a '/'.", + ); + + final Uri currentUri = value.uri; + Uri newUri = Uri.parse( + currentUri.path.endsWith('/') + ? '${currentUri.path}$relativeLocation' + : '${currentUri.path}/$relativeLocation', + ); + newUri = newUri.replace(queryParameters: { + ...currentUri.queryParameters, + ...newUri.queryParameters, + }); + + _setValue( + newUri.toString(), + RouteInformationState( + extra: extra, + type: NavigatingType.go, + ), + ); + } + /// Restores the current route matches with the `matchList`. void restore(String location, {required RouteMatchList matchList}) { _setValue( diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index c137022b802..f5f654ce475 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -24,6 +24,13 @@ extension GoRouterHelper on BuildContext { void go(String location, {Object? extra}) => GoRouter.of(this).go(location, extra: extra); + /// Navigate relative to a location. + void goRelative(String location, {Object? extra}) => + GoRouter.of(this).goRelative( + location, + extra: extra, + ); + /// Navigate to a named route. void goNamed( String name, { diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index b10f2b11158..6e8716a574d 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -365,6 +365,17 @@ class TypedGoRoute extends TypedRoute { final List> routes; } +/// A superclass for each typed go route descendant +@Target({TargetKind.library, TargetKind.classType}) +class TypedRelativeGoRoute extends TypedGoRoute { + /// Default const constructor + const TypedRelativeGoRoute({ + required super.path, + super.name, + super.routes = const >[], + }); +} + /// A superclass for each typed shell route descendant @Target({TargetKind.library, TargetKind.classType}) class TypedShellRoute extends TypedRoute { diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 9d4564eb69c..fa7d48e0c82 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -344,6 +344,15 @@ class GoRouter implements RouterConfig { routeInformationProvider.go(location, extra: extra); } + /// Navigate to a URI location by appending [relativeLocation] to the current [GoRouterState.matchedLocation] w/ optional query parameters, e.g. + void goRelative( + String relativeLocation, { + Object? extra, + }) { + log('going relative to $relativeLocation'); + routeInformationProvider.goRelative(relativeLocation, extra: extra); + } + /// Restore the RouteMatchList void restore(RouteMatchList matchList) { log('restoring ${matchList.uri}'); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index f395faf906f..3598bfe2ade 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1791,6 +1791,283 @@ void main() { }); }); + group('go relative', () { + testWidgets('from default route', (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'login', + builder: (BuildContext context, GoRouterState state) => + const LoginScreen(), + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + router.goRelative('login'); + await tester.pumpAndSettle(); + expect(find.byType(LoginScreen), findsOneWidget); + }); + + testWidgets('from non-default route', (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'login', + builder: (BuildContext context, GoRouterState state) => + const LoginScreen(), + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + router.go('/home'); + router.goRelative('login'); + await tester.pumpAndSettle(); + expect(find.byType(LoginScreen), findsOneWidget); + }); + + testWidgets('match w/ path params', (WidgetTester tester) async { + const String fid = 'f2'; + const String pid = 'p1'; + + final List routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (BuildContext context, GoRouterState state) { + expect(state.pathParameters, + {'fid': fid, 'pid': pid}); + return const PersonScreen('dummy', 'dummy'); + }, + ), + ], + ), + ], + ), + ]; + + final GoRouter router = + await createRouter(routes, tester, initialLocation: '/home'); + router.go('/'); + + router.goRelative('family/$fid'); + await tester.pumpAndSettle(); + expect(find.byType(FamilyScreen), findsOneWidget); + + router.goRelative('person/$pid'); + await tester.pumpAndSettle(); + expect(find.byType(PersonScreen), findsOneWidget); + }); + + testWidgets('match w/ query params', (WidgetTester tester) async { + const String fid = 'f2'; + const String pid = 'p1'; + + final List routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + GoRoute( + path: 'person', + builder: (BuildContext context, GoRouterState state) { + expect(state.uri.queryParameters, + {'fid': fid, 'pid': pid}); + return const PersonScreen('dummy', 'dummy'); + }, + ), + ], + ), + ], + ), + ]; + + final GoRouter router = + await createRouter(routes, tester, initialLocation: '/home'); + + router.goRelative('family?fid=$fid'); + await tester.pumpAndSettle(); + expect(find.byType(FamilyScreen), findsOneWidget); + + router.goRelative('person?pid=$pid'); + await tester.pumpAndSettle(); + expect(find.byType(PersonScreen), findsOneWidget); + }); + + testWidgets('too few params', (WidgetTester tester) async { + const String pid = 'p1'; + + final List routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (BuildContext context, GoRouterState state) => + const PersonScreen('dummy', 'dummy'), + ), + ], + ), + ], + ), + ]; + // await expectLater(() async { + final GoRouter router = await createRouter( + routes, + tester, + initialLocation: '/home', + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), + ); + router.goRelative('family/person/$pid'); + await tester.pumpAndSettle(); + expect(find.byType(TestErrorScreen), findsOneWidget); + + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); + }); + + testWidgets('match no route', (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + GoRoute( + path: 'person', + builder: (BuildContext context, GoRouterState state) => + const PersonScreen('dummy', 'dummy'), + ), + ], + ), + ], + ), + ]; + + final GoRouter router = await createRouter( + routes, + tester, + initialLocation: '/home', + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), + ); + router.go('person'); + + await tester.pumpAndSettle(); + expect(find.byType(TestErrorScreen), findsOneWidget); + + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); + }); + + testWidgets('preserve path param spaces and slashes', + (WidgetTester tester) async { + const String param1 = 'param w/ spaces and slashes'; + final List routes = [ + GoRoute( + path: '/home', + builder: dummy, + routes: [ + GoRoute( + path: 'page1/:param1', + builder: (BuildContext c, GoRouterState s) { + expect(s.pathParameters['param1'], param1); + return const DummyScreen(); + }, + ), + ], + ) + ]; + + final GoRouter router = + await createRouter(routes, tester, initialLocation: '/home'); + final String loc = 'page1/${Uri.encodeComponent(param1)}'; + router.goRelative(loc); + + await tester.pumpAndSettle(); + expect(find.byType(DummyScreen), findsOneWidget); + + final RouteMatchList matches = router.routerDelegate.currentConfiguration; + expect(matches.pathParameters['param1'], param1); + }); + + testWidgets('preserve query param spaces and slashes', + (WidgetTester tester) async { + const String param1 = 'param w/ spaces and slashes'; + final List routes = [ + GoRoute( + path: '/home', + builder: dummy, + routes: [ + GoRoute( + path: 'page1', + builder: (BuildContext c, GoRouterState s) { + expect(s.uri.queryParameters['param1'], param1); + return const DummyScreen(); + }, + ), + ], + ) + ]; + + final GoRouter router = + await createRouter(routes, tester, initialLocation: '/home'); + + router.goRelative(Uri( + path: 'page1', + queryParameters: {'param1': param1}, + ).toString()); + + await tester.pumpAndSettle(); + expect(find.byType(DummyScreen), findsOneWidget); + + final RouteMatchList matches = router.routerDelegate.currentConfiguration; + expect(matches.uri.queryParameters['param1'], param1); + }); + }); + group('redirects', () { testWidgets('top-level redirect', (WidgetTester tester) async { final List routes = [ 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 e094d1f98ed..1032b78873d 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 0cfa7a3928c..f4ca278b908 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -424,6 +424,233 @@ 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.name, + 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 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); + + void goRelative(BuildContext context) => + context.goRelative(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + Future push(BuildContext context) => + context.push(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + void replace(BuildContext context) => + context.replace(location${_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)}, + ${name != null ? 'name: ${escapeDartString(name!)},' : ''} + ${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 +777,25 @@ 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 ConstantReader nameValue = reader.read('name'); + value = GoRelativeRouteConfig._( + path: pathValue.stringValue, + name: nameValue.isNull ? null : nameValue.stringValue, + routeDataClass: classElement, + parent: parent, + parentNavigatorKey: _generateParameterGetterCode( + classElement, + parameterName: r'$parentNavigatorKey', + ), + ); default: throw UnsupportedError('Unrecognized type $typeName'); } From f397458026c9525c6971139caa26cc882844089e Mon Sep 17 00:00:00 2001 From: Thang Date: Tue, 28 May 2024 23:56:47 +0700 Subject: [PATCH 3/5] Revert "add `transitionDelegate` to `GoRouter` and penetrate it through layers to pass to `Navigator`" This reverts commit 1884e2c34e6009ef6d64a8273a651692b003709d. --- packages/go_router/lib/src/builder.dart | 10 ---------- packages/go_router/lib/src/delegate.dart | 2 -- packages/go_router/lib/src/router.dart | 4 ---- 3 files changed, 16 deletions(-) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 8a5c7d73349..49b6372c693 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -58,7 +58,6 @@ class RouteBuilder { required this.observers, required this.onPopPageWithRouteMatch, this.requestFocus = true, - this.transitionDelegate, }); /// Builder function for a go router with Navigator. @@ -95,9 +94,6 @@ class RouteBuilder { /// If this method returns false, this builder aborts the pop. final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; - /// Pass this down to [Navigator.transitionDelegate] - final TransitionDelegate? transitionDelegate; - /// Builds the top-level Navigator for the given [RouteMatchList]. Widget build( BuildContext context, @@ -122,7 +118,6 @@ class RouteBuilder { configuration: configuration, errorBuilder: errorBuilder, errorPageBuilder: errorPageBuilder, - transitionDelegate: transitionDelegate, ), ); } @@ -140,7 +135,6 @@ class _CustomNavigator extends StatefulWidget { required this.configuration, required this.errorBuilder, required this.errorPageBuilder, - required this.transitionDelegate, }); final GlobalKey navigatorKey; @@ -158,7 +152,6 @@ class _CustomNavigator extends StatefulWidget { final String? navigatorRestorationId; final GoRouterWidgetBuilder? errorBuilder; final GoRouterPageBuilder? errorPageBuilder; - final TransitionDelegate? transitionDelegate; @override State createState() => _CustomNavigatorState(); @@ -293,7 +286,6 @@ class _CustomNavigatorState extends State<_CustomNavigator> { // This is used to recursively build pages under this shell route. errorBuilder: widget.errorBuilder, errorPageBuilder: widget.errorPageBuilder, - transitionDelegate: widget.transitionDelegate, ); }, ); @@ -440,8 +432,6 @@ class _CustomNavigatorState extends State<_CustomNavigator> { pages: _pages!, observers: widget.observers, onPopPage: _handlePopPage, - transitionDelegate: widget.transitionDelegate ?? - const DefaultTransitionDelegate(), ), ), ); diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 1dbde6227ce..eba3ef0c801 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -28,7 +28,6 @@ class GoRouterDelegate extends RouterDelegate required this.routerNeglect, String? restorationScopeId, bool requestFocus = true, - TransitionDelegate? transitionDelegate, }) : _configuration = configuration { builder = RouteBuilder( configuration: configuration, @@ -39,7 +38,6 @@ class GoRouterDelegate extends RouterDelegate observers: observers, onPopPageWithRouteMatch: _handlePopPageWithRouteMatch, requestFocus: requestFocus, - transitionDelegate: transitionDelegate, ); } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index fa7d48e0c82..0146a1ca29c 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -138,7 +138,6 @@ class GoRouter implements RouterConfig { GlobalKey? navigatorKey, String? restorationScopeId, bool requestFocus = true, - TransitionDelegate? transitionDelegate, }) { return GoRouter.routingConfig( routingConfig: _ConstantRoutingConfig( @@ -161,7 +160,6 @@ class GoRouter implements RouterConfig { navigatorKey: navigatorKey, restorationScopeId: restorationScopeId, requestFocus: requestFocus, - transitionDelegate: transitionDelegate, ); } @@ -184,7 +182,6 @@ class GoRouter implements RouterConfig { GlobalKey? navigatorKey, String? restorationScopeId, bool requestFocus = true, - TransitionDelegate? transitionDelegate, }) : _routingConfig = routingConfig, backButtonDispatcher = RootBackButtonDispatcher(), assert( @@ -245,7 +242,6 @@ class GoRouter implements RouterConfig { ], restorationScopeId: restorationScopeId, requestFocus: requestFocus, - transitionDelegate: transitionDelegate, // wrap the returned Navigator to enable GoRouter.of(context).go() et al, // allowing the caller to wrap the navigator themselves builderWithNav: (BuildContext context, Widget child) => From f411cebd3fdc21658cfb4b897179da5e16cfffdf Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 00:19:28 +0700 Subject: [PATCH 4/5] TypedRelativeGoRoute shouldn't inherit TypedGoRoute, and it should only be goRelative-able --- packages/go_router/lib/src/route_data.dart | 19 +++++++++++++++---- .../lib/src/route_config.dart | 15 ++++----------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index 6e8716a574d..59ff3893052 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -367,13 +367,24 @@ class TypedGoRoute extends TypedRoute { /// A superclass for each typed go route descendant @Target({TargetKind.library, TargetKind.classType}) -class TypedRelativeGoRoute extends TypedGoRoute { +class TypedRelativeGoRoute extends TypedRoute { /// Default const constructor const TypedRelativeGoRoute({ - required super.path, - super.name, - super.routes = const >[], + 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 diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index f4ca278b908..d3724ca42ed 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; } @@ -598,15 +600,6 @@ extension $_extensionName on $_className { void goRelative(BuildContext context) => context.goRelative(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); - - Future push(BuildContext context) => - context.push(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); - - void pushReplacement(BuildContext context) => - context.pushReplacement(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); - - void replace(BuildContext context) => - context.replace(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); } '''; From c6de77298f06cd80026e76014be09aca396e4689 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 00:43:28 +0700 Subject: [PATCH 5/5] update versions go_router & go_router_builder --- packages/go_router/CHANGELOG.md | 4 ++++ packages/go_router/pubspec.yaml | 2 +- packages/go_router_builder/CHANGELOG.md | 4 ++++ packages/go_router_builder/pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 7b3e451ba90..96a991198bc 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,5 +1,9 @@ ## 14.1.3 +- Adds `GoRouter.goRelative` + +## 14.1.3 + - Improves the logging of routes when `debugLogDiagnostics` is enabled or `debugKnownRoutes() is called. Explains the position of shell routes in the route tree. Prints the widget name of the routes it is building. ## 14.1.2 diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 578b394be77..4d8331d16a6 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.1.3 +version: 14.1.4 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 3332a70e1b8..8357de4d3f0 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.7.1 + +- Adds `TypedRelativeGoRoute` annotation which supports relative routes. + ## 2.7.0 - Adds an example and a test with `onExit`. diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 0e6e10b6b3d..6fba2f8dc9f 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.0 +version: 2.7.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