From 37be48ced68d4629ca86dfe59260e6106583675f Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 15:12:19 +0700 Subject: [PATCH 01/20] - Adds `GoRouter.goRelative` - Adds `TypedRelativeGoRoute` --- .../lib/src/information_provider.dart | 27 ++ .../go_router/lib/src/misc/extensions.dart | 7 + packages/go_router/lib/src/route_data.dart | 22 ++ packages/go_router/lib/src/router.dart | 9 + packages/go_router/test/go_router_test.dart | 277 ++++++++++++++++++ 5 files changed, 342 insertions(+) diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index dc979193b325..999cd530a25a 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 c137022b8020..f5f654ce4753 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 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/lib/src/router.dart b/packages/go_router/lib/src/router.dart index dc6d88057eb0..0146a1ca29c7 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -340,6 +340,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 f395faf906fd..3598bfe2ade7 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 = [ From f8061ce550d14d2a83e1fc5238c73fbc1ec2917d Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 15:27:23 +0700 Subject: [PATCH 02/20] add go_relative example for go_router --- .../go_router/example/lib/go_relative.dart | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 packages/go_router/example/lib/go_relative.dart diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart new file mode 100644 index 000000000000..114e3b2fdff0 --- /dev/null +++ b/packages/go_router/example/lib/go_relative.dart @@ -0,0 +1,120 @@ +// 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:go_router/go_router.dart'; + +/// This sample app demonstrates how to use GoRoute.goRelative. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(); + }, + routes: [ + GoRoute( + path: 'settings', + builder: (BuildContext context, GoRouterState state) { + return const SettingsScreen(); + }, + ), + ], + ), + ], + ), + ], +); + +/// 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 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: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.goRelative('details'), + child: const Text('Go to the Details screen'), + ), + ], + ), + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details Screen')), + body: Center( + child: Column( + children: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: const Text('go back'), + ), + TextButton( + onPressed: () { + context.goRelative('settings'); + }, + child: const Text('go to settings'), + ), + ], + )), + ); + } +} + +/// The settings screen +class SettingsScreen extends StatelessWidget { + /// Constructs a [SettingsScreen] + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Settings Screen')), + body: const Center( + child: Text('Settings'), + ), + ); + } +} From 8a771474c327d8e5f8b7b04a0df106729a439ecb Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 15:32:11 +0700 Subject: [PATCH 03/20] add go_relative_test for the example --- .../go_router/example/lib/go_relative.dart | 2 +- .../example/test/go_relative_test.dart | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 packages/go_router/example/test/go_relative_test.dart diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart index 114e3b2fdff0..20f42dbb6ef7 100644 --- a/packages/go_router/example/lib/go_relative.dart +++ b/packages/go_router/example/lib/go_relative.dart @@ -95,7 +95,7 @@ class DetailsScreen extends StatelessWidget { onPressed: () { context.goRelative('settings'); }, - child: const Text('go to settings'), + child: const Text('Go to the Settings screen'), ), ], )), diff --git a/packages/go_router/example/test/go_relative_test.dart b/packages/go_router/example/test/go_relative_test.dart new file mode 100644 index 000000000000..fbc884d55594 --- /dev/null +++ b/packages/go_router/example/test/go_relative_test.dart @@ -0,0 +1,21 @@ +// 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_examples/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); + + 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); + }); +} From 61127f335bd325e62230a13393b6ca7ebb29296b Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 17:11:09 +0700 Subject: [PATCH 04/20] fix failed test --- packages/go_router/test/go_router_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 3598bfe2ade7..23b99452fbff 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1869,7 +1869,6 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.go('/'); router.goRelative('family/$fid'); await tester.pumpAndSettle(); From 687b332e135c3044b73edecad09fcc971acf3fec Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 19:45:39 +0700 Subject: [PATCH 05/20] replace goRelative with go('./$path') --- .../go_router/example/lib/go_relative.dart | 4 ++-- .../lib/src/information_provider.dart | 10 +++++++-- .../go_router/lib/src/misc/extensions.dart | 7 ------- packages/go_router/lib/src/router.dart | 9 -------- packages/go_router/test/go_router_test.dart | 21 ++++++++++--------- 5 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart index 20f42dbb6ef7..54f17293d2f1 100644 --- a/packages/go_router/example/lib/go_relative.dart +++ b/packages/go_router/example/lib/go_relative.dart @@ -63,7 +63,7 @@ class HomeScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: () => context.goRelative('details'), + onPressed: () => context.go('./details'), child: const Text('Go to the Details screen'), ), ], @@ -93,7 +93,7 @@ class DetailsScreen extends StatelessWidget { ), TextButton( onPressed: () { - context.goRelative('settings'); + context.go('./settings'); }, child: const Text('Go to the Settings screen'), ), diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 999cd530a25a..6588d07e35dc 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'match.dart'; +import 'path_utils.dart'; /// The type of the navigation. /// @@ -135,11 +136,16 @@ class GoRouteInformationProvider extends RouteInformationProvider } void _setValue(String location, Object state) { - final Uri uri = Uri.parse(location); + Uri uri = Uri.parse(location); + + // Check for relative location + if (location.startsWith('./')) { + uri = concatenateUris(_value.uri, uri); + } final bool shouldNotify = _valueHasChanged(newLocationUri: uri, newState: state); - _value = RouteInformation(uri: Uri.parse(location), state: state); + _value = RouteInformation(uri: uri, state: state); if (shouldNotify) { notifyListeners(); } diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index f5f654ce4753..c137022b8020 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -24,13 +24,6 @@ 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/router.dart b/packages/go_router/lib/src/router.dart index 0146a1ca29c7..dc6d88057eb0 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -340,15 +340,6 @@ 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 23b99452fbff..d8a6316faecb 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1809,7 +1809,7 @@ void main() { ]; final GoRouter router = await createRouter(routes, tester); - router.goRelative('login'); + router.go('./login'); await tester.pumpAndSettle(); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -1832,7 +1832,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/home'); - router.goRelative('login'); + router.go('./login'); await tester.pumpAndSettle(); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -1870,11 +1870,11 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.goRelative('family/$fid'); + router.go('./family/$fid'); await tester.pumpAndSettle(); expect(find.byType(FamilyScreen), findsOneWidget); - router.goRelative('person/$pid'); + router.go('./person/$pid'); await tester.pumpAndSettle(); expect(find.byType(PersonScreen), findsOneWidget); }); @@ -1911,11 +1911,11 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.goRelative('family?fid=$fid'); + router.go('./family?fid=$fid'); await tester.pumpAndSettle(); expect(find.byType(FamilyScreen), findsOneWidget); - router.goRelative('person?pid=$pid'); + router.go('./person?pid=$pid'); await tester.pumpAndSettle(); expect(find.byType(PersonScreen), findsOneWidget); }); @@ -1952,7 +1952,7 @@ void main() { errorBuilder: (BuildContext context, GoRouterState state) => TestErrorScreen(state.error!), ); - router.goRelative('family/person/$pid'); + router.go('./family/person/$pid'); await tester.pumpAndSettle(); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -2023,7 +2023,7 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); final String loc = 'page1/${Uri.encodeComponent(param1)}'; - router.goRelative(loc); + router.go('./$loc'); await tester.pumpAndSettle(); expect(find.byType(DummyScreen), findsOneWidget); @@ -2054,10 +2054,11 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.goRelative(Uri( + final String loc = Uri( path: 'page1', queryParameters: {'param1': param1}, - ).toString()); + ).toString(); + router.go('./$loc'); await tester.pumpAndSettle(); expect(find.byType(DummyScreen), findsOneWidget); From cca14548d881fa029dd073ff6f1a6c7047287b79 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:01:24 +0700 Subject: [PATCH 06/20] Commit missing files during merge --- .../lib/src/information_provider.dart | 27 ------------------- packages/go_router/lib/src/path_utils.dart | 15 +++++++++++ 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 6588d07e35dc..a019761922f8 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -178,33 +178,6 @@ 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/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 49a7b33b3ebc..d2c3fab1e872 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -119,6 +119,21 @@ String concatenatePaths(String parentPath, String childPath) { return '${parentPath == '/' ? '' : parentPath}/$childPath'; } +/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths , then merges their query parameters. +/// +/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?fid=1&pid=2. +Uri concatenateUris(Uri parentUri, Uri childUri) { + final Uri newUri = parentUri.replace( + path: concatenatePaths(parentUri.path, childUri.path), + queryParameters: { + ...parentUri.queryParameters, + ...childUri.queryParameters, + }, + ); + + return newUri; +} + /// Normalizes the location string. String canonicalUri(String loc) { if (loc.isEmpty) { From 975128b6fa1170dc3839c78346e1d985f0bd9c19 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:05:26 +0700 Subject: [PATCH 07/20] update changelog & version --- packages/go_router/CHANGELOG.md | 4 ++++ packages/go_router/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index c8eaab75a704..1e03cbf98ddf 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.2.1 +- Allows going to a path relatively by prefixing `./` +- Adds `TypedRelativeGoRoute` + ## 14.2.0 - Added proper `redirect` handling for `ShellRoute.$route` and `StatefulShellRoute.$route` for proper redirection handling in case of code generation. diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index eec6806cf6da..fc4f35083456 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.2.0 +version: 14.2.1 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 From 6d90488b1de7922301138730366a9ba513497a7e Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:19:23 +0700 Subject: [PATCH 08/20] Prevent concatenateUris from adding trailing redundant '?'. Add test for `concatenateUris` --- packages/go_router/lib/src/path_utils.dart | 15 +++++++++++---- packages/go_router/test/path_utils_test.dart | 12 ++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index d2c3fab1e872..268b48bee1e8 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -123,12 +123,19 @@ String concatenatePaths(String parentPath, String childPath) { /// /// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?fid=1&pid=2. Uri concatenateUris(Uri parentUri, Uri childUri) { + // Merge query parameters from both Uris. We don't return an empty map to prevent trailing '?'. + final Map? newParameters = + parentUri.queryParameters.isNotEmpty && + childUri.queryParameters.isNotEmpty + ? { + ...parentUri.queryParameters, + ...childUri.queryParameters, + } + : null; + final Uri newUri = parentUri.replace( path: concatenatePaths(parentUri.path, childUri.path), - queryParameters: { - ...parentUri.queryParameters, - ...childUri.queryParameters, - }, + queryParameters: newParameters, ); return newUri; diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index 5a656df135eb..881a0b6ae046 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -93,6 +93,18 @@ void main() { verifyThrows('', ''); }); + test('concatenateUris', () { + void verify(String pathA, String pathB, String expected) { + final String result = + concatenateUris(Uri.parse(pathA), Uri.parse(pathB)).toString(); + expect(result, expected); + } + + verify('/a', 'b/c', '/a/b/c'); + verify('/', 'b', '/b'); + verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?fid=f1&pid=p2'); + }); + test('canonicalUri', () { void verify(String path, String expected) => expect(canonicalUri(path), expected); From ccb38f92f15478dc9f26ed60a1d82385f0350169 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:27:35 +0700 Subject: [PATCH 09/20] Add more `concatenateUris` test. Fix joining params --- packages/go_router/lib/src/path_utils.dart | 2 +- packages/go_router/test/path_utils_test.dart | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 268b48bee1e8..19de05d8da0b 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -125,7 +125,7 @@ String concatenatePaths(String parentPath, String childPath) { Uri concatenateUris(Uri parentUri, Uri childUri) { // Merge query parameters from both Uris. We don't return an empty map to prevent trailing '?'. final Map? newParameters = - parentUri.queryParameters.isNotEmpty && + parentUri.queryParameters.isNotEmpty || childUri.queryParameters.isNotEmpty ? { ...parentUri.queryParameters, diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index 881a0b6ae046..78e11dd67f37 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -102,6 +102,10 @@ void main() { verify('/a', 'b/c', '/a/b/c'); verify('/', 'b', '/b'); + + // Test with parameters + verify('/a?fid=f1', 'b/c', '/a/b/c?fid=f1'); + verify('/a', 'b/c?pid=p2', '/a/b/c?pid=p2'); verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?fid=f1&pid=p2'); }); From 80d85d65202493159fd2735e0a58f14a35a706b8 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 10 Jul 2024 11:13:08 +0700 Subject: [PATCH 10/20] update example description --- packages/go_router/example/lib/go_relative.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart index 54f17293d2f1..f59faa9e76ce 100644 --- a/packages/go_router/example/lib/go_relative.dart +++ b/packages/go_router/example/lib/go_relative.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -/// This sample app demonstrates how to use GoRoute.goRelative. +/// This sample app demonstrates how to use go relatively with GoRouter.go('./$path'). void main() => runApp(const MyApp()); /// The route configuration. From a09aa94a82a01ae7d2b3beedaaead6f9e4e065e3 Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 19 Jul 2024 18:31:53 +0700 Subject: [PATCH 11/20] Make concatenateUris not merging parameters, only take them from childUri --- packages/go_router/lib/src/path_utils.dart | 20 ++++++-------------- packages/go_router/test/go_router_test.dart | 2 +- packages/go_router/test/path_utils_test.dart | 4 ++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 19de05d8da0b..247f17db14cd 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -119,25 +119,17 @@ String concatenatePaths(String parentPath, String childPath) { return '${parentPath == '/' ? '' : parentPath}/$childPath'; } -/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths , then merges their query parameters. +/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths, and take only the child's parameters. /// -/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?fid=1&pid=2. +/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?pid=2. Uri concatenateUris(Uri parentUri, Uri childUri) { - // Merge query parameters from both Uris. We don't return an empty map to prevent trailing '?'. - final Map? newParameters = - parentUri.queryParameters.isNotEmpty || - childUri.queryParameters.isNotEmpty - ? { - ...parentUri.queryParameters, - ...childUri.queryParameters, - } - : null; - - final Uri newUri = parentUri.replace( + Uri newUri = parentUri.replace( path: concatenatePaths(parentUri.path, childUri.path), - queryParameters: newParameters, + queryParameters: childUri.queryParameters, ); + // Parse the new normalized uri to remove unnecessary parts, like the trailing '?'. + newUri = Uri.parse(canonicalUri(newUri.toString())); return newUri; } diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index d8a6316faecb..eea3a8fdef6d 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1898,7 +1898,7 @@ void main() { path: 'person', builder: (BuildContext context, GoRouterState state) { expect(state.uri.queryParameters, - {'fid': fid, 'pid': pid}); + {'pid': pid}); return const PersonScreen('dummy', 'dummy'); }, ), diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index 78e11dd67f37..d25a56f246eb 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -104,9 +104,9 @@ void main() { verify('/', 'b', '/b'); // Test with parameters - verify('/a?fid=f1', 'b/c', '/a/b/c?fid=f1'); + verify('/a?fid=f1', 'b/c', '/a/b/c'); verify('/a', 'b/c?pid=p2', '/a/b/c?pid=p2'); - verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?fid=f1&pid=p2'); + verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?pid=p2'); }); test('canonicalUri', () { From a174cbe73b6f5c8de2ab08f53a30aa1a8c56878b Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 10 Jul 2024 11:26:36 +0700 Subject: [PATCH 12/20] Add GoRelativeRouteConfig --- packages/go_router_builder/CHANGELOG.md | 4 + .../lib/src/go_router_generator.dart | 1 + .../lib/src/route_config.dart | 236 +++++++++++++++++- packages/go_router_builder/pubspec.yaml | 2 +- 4 files changed, 240 insertions(+), 3 deletions(-) diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 3332a70e1b8d..8357de4d3f0a 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/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..f10e6ff26559 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,219 @@ 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)}'; + + void go(BuildContext context) => + context.go(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)}, + ${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 +765,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 0e6e10b6b3d6..1edee2d1586f 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.8.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 From 4cb033487e7d02b5d046bceb55b364faa64c1833 Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 19 Jul 2024 19:54:04 +0700 Subject: [PATCH 13/20] Add test, examples and example tests for go_router_builder. Temporarily use dependency_overrides to take local go_router --- .../example/lib/go_relative.dart | 156 ++++++++++++++++++ .../example/lib/go_relative.g.dart | 71 ++++++++ .../go_router_builder/example/pubspec.yaml | 4 + .../example/test/go_relative_test.dart | 29 ++++ packages/go_router_builder/pubspec.yaml | 4 + .../test_inputs/go_relative.dart | 37 +++++ .../test_inputs/go_relative.dart.expect | 88 ++++++++++ 7 files changed, 389 insertions(+) create mode 100644 packages/go_router_builder/example/lib/go_relative.dart create mode 100644 packages/go_router_builder/example/lib/go_relative.g.dart create mode 100644 packages/go_router_builder/example/test/go_relative_test.dart create mode 100644 packages/go_router_builder/test_inputs/go_relative.dart create mode 100644 packages/go_router_builder/test_inputs/go_relative.dart.expect 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..640523161bf2 --- /dev/null +++ b/packages/go_router_builder/example/lib/go_relative.dart @@ -0,0 +1,156 @@ +// 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: >[detailRoute], +) +class HomeRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const HomeScreen(); + } +} + +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 StatefulWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: ElevatedButton( + onPressed: () { + const DetailsRoute(detailId: 'DetailsId').go(context); + }, + child: const Text('Go to the Details screen'), + ), + ), + ); + } +} + +/// 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: () => HomeRoute().go(context), + child: const Text('Go back'), + ), + ElevatedButton( + onPressed: () => const SettingsRoute( + settingId: 'SettingsId', + ).go(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..2df63906645b --- /dev/null +++ b/packages/go_router_builder/example/lib/go_relative.g.dart @@ -0,0 +1,71 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'go_relative.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $homeRoute, + ]; + +RouteBase get $homeRoute => GoRouteData.$route( + path: '/', + factory: $HomeRouteExtension._fromState, + routes: [ + 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 $DetailsRouteExtension on DetailsRoute { + static DetailsRoute _fromState(GoRouterState state) => DetailsRoute( + detailId: state.pathParameters['detailId']!, + ); + + String get location => './${GoRouteData.$location( + 'details/${Uri.encodeComponent(detailId)}', + )}'; + + void go(BuildContext context) => context.go(location); +} + +extension $SettingsRouteExtension on SettingsRoute { + static SettingsRoute _fromState(GoRouterState state) => SettingsRoute( + settingId: state.pathParameters['settingId']!, + ); + + String get location => './${GoRouteData.$location( + 'settings/${Uri.encodeComponent(settingId)}', + )}'; + + void go(BuildContext context) => context.go(location); +} diff --git a/packages/go_router_builder/example/pubspec.yaml b/packages/go_router_builder/example/pubspec.yaml index bc02b13402eb..18e225d06812 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..23292e8ffd7e --- /dev/null +++ b/packages/go_router_builder/example/test/go_relative_test.dart @@ -0,0 +1,29 @@ +// 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); + + 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); + }); +} diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 1edee2d1586f..76d904c065a3 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -29,6 +29,10 @@ dev_dependencies: go_router: ^14.0.0 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..d491d6562add --- /dev/null +++ b/packages/go_router_builder/test_inputs/go_relative.dart.expect @@ -0,0 +1,88 @@ +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', + )}'; + + void go(BuildContext context) => context.go(location); +} + +extension $InnerRelativeRouteExtension on InnerRelativeRoute { + static InnerRelativeRoute _fromState(GoRouterState state) => + const InnerRelativeRoute(); + + String get location => './${GoRouteData.$location( + 'inner-relative-route', + )}'; + + void go(BuildContext context) => context.go(location); +} + +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); +} From ceaa318a0f1fa12493e0c214e3da68cb3e11a277 Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 19 Jul 2024 21:38:53 +0700 Subject: [PATCH 14/20] Add relativeLocation, push, pushReplacement & replace to the RelativeRoute's extension --- .../go_router_builder/lib/src/route_config.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index f10e6ff26559..1236dc265349 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -592,10 +592,20 @@ class GoRelativeRouteConfig extends RouteBaseConfig { extension $_extensionName on $_className { static $_className _fromState(GoRouterState state) $_fromStateConstructor - String get location => './\${GoRouteData.\$location($_locationArgs,$_locationQueryParams)}'; + String get location => GoRouteData.\$location($_locationArgs,$_locationQueryParams); + String get relativeLocation => './\$location'; void go(BuildContext context) => - context.go(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); + context.go(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + Future push(BuildContext context) => + context.push(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + void pushReplacement(BuildContext context) => + context.pushReplacement(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); + + void replace(BuildContext context) => + context.replace(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); } '''; From 07b4a238dabf633f55fbd9d8dd7fe24c587573f8 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 22 Jan 2025 15:06:59 +0700 Subject: [PATCH 15/20] update go_router package dependency --- packages/go_router_builder/pubspec.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 8c6d24360abf..4463cce2aa0c 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -26,16 +26,12 @@ 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 From 6008b8bb6a59a2d0e76fc11f80769f828391e6c8 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 22 Jan 2025 15:17:33 +0700 Subject: [PATCH 16/20] Fix merge issues --- packages/go_router/CHANGELOG.md | 1 + packages/go_router/lib/src/path_utils.dart | 14 -------------- packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/path_utils_test.dart | 16 ---------------- 4 files changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 4c631d7caa63..fc8d5b8d548e 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,4 +1,5 @@ ## 14.6.5 + - Adds `TypedRelativeGoRoute` ## 14.6.4 diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 035fe774ce14..8bcb62a3c38e 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -126,20 +126,6 @@ Uri concatenateUris(Uri parentUri, Uri childUri) { return newUri; } -/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths, and take only the child's parameters. -/// -/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?pid=2. -Uri concatenateUris(Uri parentUri, Uri childUri) { - Uri newUri = parentUri.replace( - path: concatenatePaths(parentUri.path, childUri.path), - queryParameters: childUri.queryParameters, - ); - - // Parse the new normalized uri to remove unnecessary parts, like the trailing '?'. - newUri = Uri.parse(canonicalUri(newUri.toString())); - return newUri; -} - /// Normalizes the location string. String canonicalUri(String loc) { if (loc.isEmpty) { diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 20b44d038443..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.6 +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/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index 8a0eb1a9238d..b13599c74a5f 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -109,22 +109,6 @@ void main() { verify('/a?fid=f1#f', 'b/c?pid=p2#', '/a/b/c?pid=p2#'); }); - test('concatenateUris', () { - void verify(String pathA, String pathB, String expected) { - final String result = - concatenateUris(Uri.parse(pathA), Uri.parse(pathB)).toString(); - expect(result, expected); - } - - verify('/a', 'b/c', '/a/b/c'); - verify('/', 'b', '/b'); - - // Test with parameters - verify('/a?fid=f1', 'b/c', '/a/b/c'); - verify('/a', 'b/c?pid=p2', '/a/b/c?pid=p2'); - verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?pid=p2'); - }); - test('canonicalUri', () { void verify(String path, String expected) => expect(canonicalUri(path), expected); From d2fbc0ebe52aaf11cea198f094b92dc7423603ee Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 22 Jan 2025 15:32:45 +0700 Subject: [PATCH 17/20] temporarily add dependency_override --- packages/go_router_builder/pubspec.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 4463cce2aa0c..19889010b8b2 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -32,6 +32,10 @@ dev_dependencies: pub_semver: ^2.1.5 test: ^1.20.0 +dependency_overrides: + go_router: + path: ../go_router + topics: - codegen - deep-linking From 8b5fb4d4990fb050cd5f20b94fffe23c07834cf5 Mon Sep 17 00:00:00 2001 From: Thang Date: Tue, 4 Feb 2025 02:04:48 +0700 Subject: [PATCH 18/20] change relative route actions to have suffix `Relative` --- .../lib/src/route_config.dart | 8 +++--- .../test_inputs/go_relative.dart.expect | 28 +++++++++++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 1236dc265349..f341f0940ec2 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -595,16 +595,16 @@ extension $_extensionName on $_className { String get location => GoRouteData.\$location($_locationArgs,$_locationQueryParams); String get relativeLocation => './\$location'; - void go(BuildContext context) => + void goRelative(BuildContext context) => context.go(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); - Future push(BuildContext context) => + Future pushRelative(BuildContext context) => context.push(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); - void pushReplacement(BuildContext context) => + void pushReplacementRelative(BuildContext context) => context.pushReplacement(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); - void replace(BuildContext context) => + void replaceRelative(BuildContext context) => context.replace(relativeLocation${_extraParam != null ? ', extra: $extraFieldName' : ''}); } '''; diff --git a/packages/go_router_builder/test_inputs/go_relative.dart.expect b/packages/go_router_builder/test_inputs/go_relative.dart.expect index d491d6562add..5e1275f5e24b 100644 --- a/packages/go_router_builder/test_inputs/go_relative.dart.expect +++ b/packages/go_router_builder/test_inputs/go_relative.dart.expect @@ -35,22 +35,38 @@ extension $Route1Extension on Route1 { extension $RelativeRouteExtension on RelativeRoute { static RelativeRoute _fromState(GoRouterState state) => const RelativeRoute(); - String get location => './${GoRouteData.$location( + String get location => GoRouteData.$location( 'relative-route', - )}'; + ); + String get relativeLocation => './$location'; - void go(BuildContext context) => context.go(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( + String get location => GoRouteData.$location( 'inner-relative-route', - )}'; + ); + String get relativeLocation => './$location'; - void go(BuildContext context) => context.go(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( From 02c3d7b63d5fb5eb8423514984a2185cf013352a Mon Sep 17 00:00:00 2001 From: Thang Date: Tue, 4 Feb 2025 22:16:15 +0700 Subject: [PATCH 19/20] Fix example --- .../example/lib/go_relative.dart | 4 +-- .../example/lib/go_relative.g.dart | 34 ++++++++++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/go_router_builder/example/lib/go_relative.dart b/packages/go_router_builder/example/lib/go_relative.dart index 640523161bf2..31ccc8e6c3c7 100644 --- a/packages/go_router_builder/example/lib/go_relative.dart +++ b/packages/go_router_builder/example/lib/go_relative.dart @@ -86,7 +86,7 @@ class _HomeScreenState extends State { body: Center( child: ElevatedButton( onPressed: () { - const DetailsRoute(detailId: 'DetailsId').go(context); + const DetailsRoute(detailId: 'DetailsId').goRelative(context); }, child: const Text('Go to the Details screen'), ), @@ -119,7 +119,7 @@ class DetailsScreen extends StatelessWidget { ElevatedButton( onPressed: () => const SettingsRoute( settingId: 'SettingsId', - ).go(context), + ).goRelative(context), child: const Text('Go to the Settings screen'), ), ], diff --git a/packages/go_router_builder/example/lib/go_relative.g.dart b/packages/go_router_builder/example/lib/go_relative.g.dart index 2df63906645b..e76190dcca77 100644 --- a/packages/go_router_builder/example/lib/go_relative.g.dart +++ b/packages/go_router_builder/example/lib/go_relative.g.dart @@ -1,7 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: always_specify_types, public_member_api_docs - part of 'go_relative.dart'; // ************************************************************************** @@ -51,11 +49,21 @@ extension $DetailsRouteExtension on DetailsRoute { detailId: state.pathParameters['detailId']!, ); - String get location => './${GoRouteData.$location( + String get location => GoRouteData.$location( 'details/${Uri.encodeComponent(detailId)}', - )}'; + ); + String get relativeLocation => './$location'; - void go(BuildContext context) => context.go(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 { @@ -63,9 +71,19 @@ extension $SettingsRouteExtension on SettingsRoute { settingId: state.pathParameters['settingId']!, ); - String get location => './${GoRouteData.$location( + String get location => GoRouteData.$location( 'settings/${Uri.encodeComponent(settingId)}', - )}'; + ); + String get relativeLocation => './$location'; - void go(BuildContext context) => context.go(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); } From 72c06de008c84b1a371e2fa44cd1c88ce2a5ec67 Mon Sep 17 00:00:00 2001 From: Thang Date: Thu, 6 Feb 2025 20:33:19 +0700 Subject: [PATCH 20/20] Update go_relative test --- .../example/lib/go_relative.dart | 74 +++++++++++++++---- .../example/lib/go_relative.g.dart | 33 +++++++++ .../example/test/go_relative_test.dart | 26 +++++++ 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/packages/go_router_builder/example/lib/go_relative.dart b/packages/go_router_builder/example/lib/go_relative.dart index 31ccc8e6c3c7..26f49772d507 100644 --- a/packages/go_router_builder/example/lib/go_relative.dart +++ b/packages/go_router_builder/example/lib/go_relative.dart @@ -38,7 +38,13 @@ const TypedRelativeGoRoute detailRoute = @TypedGoRoute( path: '/', - routes: >[detailRoute], + routes: >[ + TypedGoRoute( + path: '/dashboard', + routes: >[detailRoute], + ), + detailRoute, + ], ) class HomeRoute extends GoRouteData { @override @@ -47,6 +53,13 @@ class HomeRoute extends GoRouteData { } } +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; @@ -70,26 +83,57 @@ class SettingsRoute extends GoRouteData { } /// The home screen -class HomeScreen extends StatefulWidget { +class HomeScreen extends StatelessWidget { /// Constructs a [HomeScreen] const HomeScreen({super.key}); @override - State createState() => _HomeScreenState(); + 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'), + ), + ], + ), + ); + } } -class _HomeScreenState extends State { +/// 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('Home Screen')), - body: Center( - child: ElevatedButton( - onPressed: () { - const DetailsRoute(detailId: 'DetailsId').goRelative(context); - }, - child: const Text('Go to the Details screen'), - ), + 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'), + ), + ], ), ); } @@ -113,7 +157,7 @@ class DetailsScreen extends StatelessWidget { child: Column( children: [ ElevatedButton( - onPressed: () => HomeRoute().go(context), + onPressed: () => context.pop(), child: const Text('Go back'), ), ElevatedButton( @@ -145,9 +189,7 @@ class SettingsScreen extends StatelessWidget { appBar: AppBar(title: Text('Settings Screen $id')), body: Center( child: TextButton( - onPressed: () { - context.pop(); - }, + 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 index e76190dcca77..1a3907739fa9 100644 --- a/packages/go_router_builder/example/lib/go_relative.g.dart +++ b/packages/go_router_builder/example/lib/go_relative.g.dart @@ -14,6 +14,22 @@ 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, @@ -44,6 +60,23 @@ extension $HomeRouteExtension on HomeRoute { 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']!, diff --git a/packages/go_router_builder/example/test/go_relative_test.dart b/packages/go_router_builder/example/test/go_relative_test.dart index 23292e8ffd7e..3efd323e8c23 100644 --- a/packages/go_router_builder/example/test/go_relative_test.dart +++ b/packages/go_router_builder/example/test/go_relative_test.dart @@ -10,6 +10,7 @@ void main() { 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); @@ -25,5 +26,30 @@ void main() { 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); }); }