Skip to content

Commit 67d8b50

Browse files
[go_router] Add support for relative routes (#6825)
Add supports for relative routes by allowing going to a path relatively, like go('./$path') This PR doesn't fully resolve any issue, but it's mandatory to further add examples & tests for `TypedRelativeGoRoute` (see [#7174](#6823)), which will resolves [#108177](flutter/flutter#108177)
1 parent e547e7a commit 67d8b50

File tree

8 files changed

+539
-3
lines changed

8 files changed

+539
-3
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 14.6.0
2+
3+
- Allows going to a path relatively by prefixing `./`
4+
15
## 14.5.0
26

37
- Adds preload support to StatefulShellRoute, configurable via `preload` parameter on StatefulShellBranch.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:go_router/go_router.dart';
7+
8+
/// This sample app demonstrates how to use go relatively with GoRouter.go('./$path').
9+
void main() => runApp(const MyApp());
10+
11+
/// The main app.
12+
class MyApp extends StatelessWidget {
13+
/// Constructs a [MyApp]
14+
const MyApp({super.key});
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return MaterialApp.router(
19+
routerConfig: _router,
20+
);
21+
}
22+
}
23+
24+
/// The route configuration.
25+
final GoRouter _router = GoRouter(
26+
routes: <RouteBase>[
27+
GoRoute(
28+
path: '/',
29+
builder: (BuildContext context, GoRouterState state) {
30+
return const HomeScreen();
31+
},
32+
routes: <RouteBase>[
33+
GoRoute(
34+
path: 'details',
35+
builder: (BuildContext context, GoRouterState state) {
36+
return const DetailsScreen();
37+
},
38+
routes: <RouteBase>[
39+
GoRoute(
40+
path: 'settings',
41+
builder: (BuildContext context, GoRouterState state) {
42+
return const SettingsScreen();
43+
},
44+
),
45+
],
46+
),
47+
],
48+
),
49+
],
50+
);
51+
52+
/// The home screen
53+
class HomeScreen extends StatelessWidget {
54+
/// Constructs a [HomeScreen]
55+
const HomeScreen({super.key});
56+
57+
@override
58+
Widget build(BuildContext context) {
59+
return Scaffold(
60+
appBar: AppBar(title: const Text('Home Screen')),
61+
body: Center(
62+
child: Column(
63+
mainAxisAlignment: MainAxisAlignment.center,
64+
children: <Widget>[
65+
ElevatedButton(
66+
onPressed: () => context.go('./details'),
67+
child: const Text('Go to the Details screen'),
68+
),
69+
],
70+
),
71+
),
72+
);
73+
}
74+
}
75+
76+
/// The details screen
77+
class DetailsScreen extends StatelessWidget {
78+
/// Constructs a [DetailsScreen]
79+
const DetailsScreen({super.key});
80+
81+
@override
82+
Widget build(BuildContext context) {
83+
return Scaffold(
84+
appBar: AppBar(title: const Text('Details Screen')),
85+
body: Center(
86+
child: Column(
87+
children: <Widget>[
88+
TextButton(
89+
onPressed: () {
90+
context.pop();
91+
},
92+
child: const Text('Go back'),
93+
),
94+
TextButton(
95+
onPressed: () {
96+
context.go('./settings');
97+
},
98+
child: const Text('Go to the Settings screen'),
99+
),
100+
],
101+
),
102+
),
103+
);
104+
}
105+
}
106+
107+
/// The settings screen
108+
class SettingsScreen extends StatelessWidget {
109+
/// Constructs a [SettingsScreen]
110+
const SettingsScreen({super.key});
111+
112+
@override
113+
Widget build(BuildContext context) {
114+
return Scaffold(
115+
appBar: AppBar(title: const Text('Settings Screen')),
116+
body: Column(
117+
children: <Widget>[
118+
TextButton(
119+
onPressed: () {
120+
context.pop();
121+
},
122+
child: const Text('Go back'),
123+
),
124+
],
125+
),
126+
);
127+
}
128+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:go_router_examples/go_relative.dart' as example;
7+
8+
void main() {
9+
testWidgets('example works', (WidgetTester tester) async {
10+
await tester.pumpWidget(const example.MyApp());
11+
expect(find.byType(example.HomeScreen), findsOneWidget);
12+
13+
await tester.tap(find.text('Go to the Details screen'));
14+
await tester.pumpAndSettle();
15+
expect(find.byType(example.DetailsScreen), findsOneWidget);
16+
17+
await tester.tap(find.text('Go to the Settings screen'));
18+
await tester.pumpAndSettle();
19+
expect(find.byType(example.SettingsScreen), findsOneWidget);
20+
21+
await tester.tap(find.text('Go back'));
22+
await tester.pumpAndSettle();
23+
expect(find.byType(example.DetailsScreen), findsOneWidget);
24+
25+
await tester.tap(find.text('Go back'));
26+
await tester.pumpAndSettle();
27+
expect(find.byType(example.HomeScreen), findsOneWidget);
28+
});
29+
}

packages/go_router/lib/src/information_provider.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
1010
import 'package:flutter/widgets.dart';
1111

1212
import 'match.dart';
13+
import 'path_utils.dart';
1314

1415
/// The type of the navigation.
1516
///
@@ -139,11 +140,16 @@ class GoRouteInformationProvider extends RouteInformationProvider
139140
}
140141

141142
void _setValue(String location, Object state) {
142-
final Uri uri = Uri.parse(location);
143+
Uri uri = Uri.parse(location);
144+
145+
// Check for relative location
146+
if (location.startsWith('./')) {
147+
uri = concatenateUris(_value.uri, uri);
148+
}
143149

144150
final bool shouldNotify =
145151
_valueHasChanged(newLocationUri: uri, newState: state);
146-
_value = RouteInformation(uri: Uri.parse(location), state: state);
152+
_value = RouteInformation(uri: uri, state: state);
147153
if (shouldNotify) {
148154
notifyListeners();
149155
}

packages/go_router/lib/src/path_utils.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'misc/errors.dart';
56
import 'route.dart';
67

78
final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
@@ -112,6 +113,54 @@ String concatenatePaths(String parentPath, String childPath) {
112113
return '/${segments.join('/')}';
113114
}
114115

116+
/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths, and take only the child's parameters.
117+
///
118+
/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?pid=2.
119+
Uri concatenateUris(Uri parentUri, Uri childUri) {
120+
Uri newUri = childUri.replace(
121+
path: concatenatePaths(parentUri.path, childUri.path),
122+
);
123+
124+
// Parse the new normalized uri to remove unnecessary parts, like the trailing '?'.
125+
newUri = Uri.parse(canonicalUri(newUri.toString()));
126+
return newUri;
127+
}
128+
129+
/// Normalizes the location string.
130+
String canonicalUri(String loc) {
131+
if (loc.isEmpty) {
132+
throw GoException('Location cannot be empty.');
133+
}
134+
String canon = Uri.parse(loc).toString();
135+
canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon;
136+
final Uri uri = Uri.parse(canon);
137+
138+
// remove trailing slash except for when you shouldn't, e.g.
139+
// /profile/ => /profile
140+
// / => /
141+
// /login?from=/ => /login?from=/
142+
canon = uri.path.endsWith('/') &&
143+
uri.path != '/' &&
144+
!uri.hasQuery &&
145+
!uri.hasFragment
146+
? canon.substring(0, canon.length - 1)
147+
: canon;
148+
149+
// replace '/?', except for first occurrence, from path only
150+
// /login/?from=/ => /login?from=/
151+
// /?from=/ => /?from=/
152+
final int pathStartIndex = uri.host.isNotEmpty
153+
? uri.toString().indexOf(uri.host) + uri.host.length
154+
: uri.hasScheme
155+
? uri.toString().indexOf(uri.scheme) + uri.scheme.length
156+
: 0;
157+
if (pathStartIndex < canon.length) {
158+
canon = canon.replaceFirst('/?', '?', pathStartIndex + 1);
159+
}
160+
161+
return canon;
162+
}
163+
115164
/// Builds an absolute path for the provided route.
116165
String? fullPathForRoute(
117166
RouteBase targetRoute, String parentFullpath, List<RouteBase> routes) {

packages/go_router/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 14.5.0
4+
version: 14.6.0
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

0 commit comments

Comments
 (0)